from typing import List, Tuple
import pandas as pd
import numpy as np
from typing import List, Dict, Optional, Any
from database import DatabaseManager
from models import create_stock_model
from sklearn.linear_model import LinearRegression
from scipy.signal import argrelextrema


def get_dataframe(code: str) -> pd.DataFrame:
    """
    指定された銘柄コードの株価データをデータベースから取得し、Pandas DataFrame 形式で返す。

    Args:
        code (str): 取得する株式の銘柄コード。

    Returns:
        pd.DataFrame: 取得した株価データの DataFrame。
                     - インデックス: `date` (datetime)
    """
    db_manager = DatabaseManager()
    session = db_manager.get_session()

    # 銘柄コードに基づくモデルクラスを作成
    StockModel = create_stock_model(f"stock_{code}", db_manager.Base)

    stock_datas = session.query(StockModel).all()
    dicts = [vars(stock_data) for stock_data in stock_datas]
    df = pd.DataFrame(dicts).drop("_sa_instance_state", axis=1)
    return df


# 移動平均線のゴールデンクロスのチェック


def check_ma_golden_cross(df: pd.DataFrame) -> bool:
    """
    指定されたDataFrame内の移動平均線を用いて、直近3日間のゴールデンクロス発生を判定する。

    Args:
        df (pd.DataFrame): MA5とMA25を含む株価データのDataFrame。

    Returns:
        bool: ゴールデンクロスが発生していればTrue、それ以外はFalse。
    """
    if len(df) < 3:  # データが不足している場合
        return False

    # 直近3日分のma5とma25のリストを作成
    ma5_3_days = df["ma5"].tail(3).tolist()  # ma5: 短期移動平均線（5日）
    ma25_3_days = df["ma25"].tail(3).tolist()  # ma25: 長期移動平均線（25日）

    # 最新日（当日）にゴールデンクロスが発生（前日だけでma5がma25以下で、当日に上回る）
    if ma5_3_days[2] > ma25_3_days[2]:  # 当日のma5がma25を上回っている
        if ma5_3_days[1] < ma25_3_days[1]:  # 前日のma5がma25を下回っている
            return True

    # 前日にゴールデンクロスが発生（2日前はma5がma25を下回り、前日に上回る）
    if ma5_3_days[1] > ma25_3_days[1]:  # 前日のma5がma25を上回っている
        if ma5_3_days[0] < ma25_3_days[0]:  # 2日前のma5がma25を下回っている
            return True

    # 上記以外
    return False


# MACDのゴールデンクロスのチェック
def check_macd_golden_cross(df: pd.DataFrame) -> bool:
    """
    指定されたDataFrame内のMACDを用いて、直近3日間のゴールデンクロス発生を判定する。

    Args:
        df (pd.DataFrame): MACDとシグナルを含む株価データのDataFrame。

    Returns:
        bool: ゴールデンクロスが発生していれば True、それ以外はFalse。
    """
    if len(df) < 3:  # データが不足している場合
        return False

    # 直近3日分のMACDとシグナルのリストを作成（降順なので最新が最初）
    macd_3_days = df["macd"].tail(3).tolist()  # MACD
    signal_3_days = df["macd_signal"].tail(3).tolist()  # シグナル

    # 最新日（当日）にMACDのゴールデンクロスが発生
    if macd_3_days[2] > signal_3_days[2]:  # 当日のMACDがシグナルを上回っている
        if macd_3_days[1] < signal_3_days[1]:  # 前日のMACDがシグナルを下回っている
            return True

    # 前日にMACDのゴールデンクロスが発生
    if macd_3_days[1] > signal_3_days[1]:  # 前日のMACDがシグナルを上回っている
        if macd_3_days[0] < signal_3_days[0]:  # 2日前のMACDがシグナルを下回っている
            return True

    # 上記以外
    return False


# RCIのゴールデンクロスのチェック
def check_rci_golden_cross(df: pd.DataFrame) -> bool:
    """
    指定されたDataFrame内のRCIを用いて、直近3日間のゴールデンクロス発生を判定する。

    Args:
        df (pd.DataFrame): RCI9とRCI26を含む株価データのDataFrame。

    Returns:
        bool: ゴールデンクロスが発生していれば True、それ以外はFalse。
    """
    if len(df) < 3:  # データが不足している場合
        return False

    # 直近3日間のRCI9とRCI26のリストを作成
    rci9_3_days = df["rci9"].tail(3).tolist()  # RCI9
    rci26_3_days = df["rci26"].tail(3).tolist()  # RCI26

    # 最新日（当日）にRCIのゴールデンクロスが発生
    if rci9_3_days[2] > rci26_3_days[2]:  # 当日のRCI9がRCI26を上回っている
        if rci9_3_days[1] < rci26_3_days[1]:  # 前日のRCI9がRCI26を下回っている
            return True

    # 前日にRCIのゴールデンクロスが発生
    if rci9_3_days[1] > rci26_3_days[1]:  # 前日のRCI9がRCI26を上回っている
        if rci9_3_days[0] < rci26_3_days[0]:  # 2日前のRCI9がRCI26を下回っている
            return True

    # 上記以外
    return False


# ・MACDがシグナルより下でゴールデンクロス前
# ・MACDが日々上昇しつつある
# ・長期RCIが-50未満
def check_rising_condition(df: pd.DataFrame) -> bool:
    """
    株価データのMACD・RCIを解析し、上昇の兆候があるかを判定する。

    判定条件:
    1. MACD がシグナルより下にある（ゴールデンクロス前の状態）。
    2. MACD が日々上昇している（直近2日間でMACDが増加傾向）。
    3. 長期RCI（RCI26）が-50未満である（売られすぎの状態）。

    Args:
        df (pd.DataFrame): MACD／シグナル／RCI26のカラムを含む株価データのDataFrame。

    Returns:
        bool: 条件を満たしていれば True、そうでなければFalse。
    """
    if len(df) < 3:  # データが不足している場合
        return False

    df = df.tail(3)
    # MACDがシグナルより下（ゴールデンクロス前）
    if df.iloc[2]["macd"] < df.iloc[2]["macd_signal"]:
        # MACDが日々上昇
        if (
            df.iloc[0]["macd"] < df.iloc[1]["macd"]
            and df.iloc[0]["macd"] < df.iloc[1]["macd"]
        ):
            # 長期RCIが-50未満
            if df.iloc[0]["rci26"] < -50:
                return True

    # 上記以外
    return False


# これから上がるパターンに1つでも合致しているかのチェック
def check_rise(df: pd.DataFrame) -> bool:
    """
    指定されたDataFrameを用いて、これから上がるパターンに合致しているかを判定する。

    Args:
        df (pd.DataFrame): 株価データのDataFrame。

    Returns:
        bool: これから上がるパターンに合致していれば True、それ以外はFalse。
    """
    return (
        check_ma_golden_cross(df)
        or check_macd_golden_cross(df)
        or check_rci_golden_cross(df)
        or check_rising_condition(df)
    )


def detect_trend_lines(
    df: pd.DataFrame, lookback: int = 200, min_touches: int = 5, order: int = 5
) -> List[Dict[str, Any]]:
    """
    指定されたデータフレームから上昇・下降トレンドラインを検出する。

    Args:
        df (pd.DataFrame): OHLCV データを含む DataFrame。
        lookback (int): 直近のデータ何本を基準にするか (デフォルト: 200)。
        min_touches (int): トレンドラインとして認定する最小の反発回数 (デフォルト: 5)。
        order (int): 極値を検出する際の比較範囲 (デフォルト: 5)。

    Returns:
        List[Dict[str, Any]]: 検出されたトレンドラインのリスト (空リストの可能性あり)。
    """
    df = df.tail(lookback)  # 直近のデータのみ使用
    highs = df["high"]
    lows = df["low"]

    # 高値と安値の極値を取得
    local_highs = df.iloc[argrelextrema(highs.values, np.greater, order=order)[0]]
    local_lows = df.iloc[argrelextrema(lows.values, np.less, order=order)[0]]

    # トレンドラインを算出
    def get_trend_line(
        extrema: pd.DataFrame, trend_type: str
    ) -> Optional[Dict[str, Any]]:
        """
        極値データを用いて、線形回帰で最適なトレンドラインを求める。

        Args:
            extrema (pd.DataFrame): 高値または安値の極値を含む DataFrame。
            trend_type (str): "low" ならサポートライン、"high" ならレジスタンスライン。

        Returns:
            Optional[Dict[str, Any]]: 検出されたトレンドライン情報を含む辞書。
        """
        if len(extrema) < 2:
            return None  # 極値が2つ未満ならトレンドラインを作れない

        extrema = extrema.copy()
        # 日付を数値に変換（回帰用）
        extrema["date_num"] = np.arange(len(extrema))

        # 線形回帰モデルを作成
        model = LinearRegression()
        # date_num（数値化した日付）を X（説明変数）、trend_type（高値 or 安値）を Y（目的変数） として学習
        model.fit(extrema[["date_num"]], extrema[trend_type])

        # 予測値を計算
        # 学習済みのモデルを使って トレンドラインの値を計算し、新しい列trend_fitに保存
        extrema["trend_fit"] = model.predict(extrema[["date_num"]])

        # 最初と最後の点を取得
        start = extrema.iloc[0]
        end = extrema.iloc[-1]

        return {
            "start": [start["date"], start["trend_fit"]],
            "end": [end["date"], end["trend_fit"]],
            "type": "uptrend" if trend_type == "low" else "downtrend",
        }

    uptrend = get_trend_line(local_lows, "low")  # サポートライン
    downtrend = get_trend_line(local_highs, "high")  # レジスタンスライン

    return [line for line in [uptrend, downtrend] if line is not None]


# 逆三尊パターンを検出
def detect_all_inverse_head_and_shoulders(
    prices: List[float],
    bottoms: List[int],
    threshold_ratio: float = 0.005,
    min_distance: int = 5,
) -> List[Tuple[int, int, int]]:
    """
    逆三尊パターンを検出する。

    Args:
        prices (List[float]): 株価のリスト（インデックスは時系列に対応）。
        bottoms (List[int]): 安値のインデックスリスト（ボトムの位置）。
        threshold_ratio (float, optional): 左右の山が中央の谷より高くなる最低比率。デフォルトは 0.005（0.5%）。
        min_distance (int, optional): 左肩・中央・右肩の間隔の最小値。デフォルトは 5。

    Returns:
        List[Tuple[int, int, int]]: 検出された逆三尊パターンのリスト。
                                    各タプルは (左肩, 中央, 右肩) のインデックスを示す。
    """
    # 結果を格納する変数
    patterns = []

    # bottomsのリストを後ろからチェック
    for i in range(len(bottoms) - 2, 0, -1):
        # bottomsのi番目の値を中央（center）とし、その前後の値を左肩（left）、右肩（right）
        left = bottoms[i - 1]
        center = bottoms[i]
        right = bottoms[i + 1]

        # 左肩 (left) と右肩 (right) の価格が、中央 (center) の価格よりも最低限高い条件
        threshold = prices[center] * threshold_ratio

        # 中央の安値よりも左肩・右肩が一定以上高いか判定
        if (
            prices[left] > prices[center] + threshold
            and prices[right] > prices[center] + threshold
        ):
            # 左肩・中央・右肩の間隔がmin_distance以上であることを確認
            if (center - left) >= min_distance and (right - center) >= min_distance:
                patterns.append((left, center, right))

    return patterns
