from flask import (
    Flask,
    Response,
    flash,
    jsonify,
    redirect,
    render_template,
    g,
    request,
    url_for,
)
from typing import Any, Dict, List
import pandas as pd
from sqlalchemy import asc, desc
from sqlalchemy.orm import aliased
from sqlalchemy.exc import SQLAlchemyError
from database import DatabaseManager
from models import create_stock_model, get_stock_class
import yaml
from util import clean_string, series_to_json_format
import yfinance as yf
from util import (
    get_dataframe,
    detect_trend_lines,
    detect_all_inverse_head_and_shoulders,
    check_ma_golden_cross,
    check_macd_golden_cross,
    check_rci_golden_cross,
    check_rising_condition,
    check_rise,
)

# Flaskアプリケーションを作成


def create_app():
    app = Flask(__name__)
    app.secret_key = "secret_key_for_flash_messages"

    @app.before_request
    def load_stock_data() -> None:
        """
        各リクエスト前に実行される関数。
        設定ファイル("config.yaml")から株価指数の設定を読み込み、
        各指数の直近2日間の終値データと株式銘柄の全データをデータベースから取得し、
        Flaskのグローバルオブジェクト(g)に 'index_data', 'stocks_all' として保存する。

        - 各指数について、直近2日分のデータが存在すれば、今日の終値とその差分を計算して格納。
        - 取得できない場合は "N/A" もしくは "Error" を設定し、エラー発生時は空の辞書とする。

        Returns:
            None
        """

        db_manager = DatabaseManager()
        g.index_data = {}
        g.stocks_all = []
        try:
            with open("config.yaml", "r", encoding="utf-8") as file:
                config = yaml.safe_load(file)
                indices = config["indices"]

                # セッションの管理を with ステートメントで実施
                with db_manager.get_session() as session:
                    for index in indices:
                        try:
                            symbol = clean_string(index["symbol"])
                            name = index["name"]
                            # 直近2日間のデータを取得
                            StockModel = create_stock_model(
                                f"stock_{symbol}", db_manager.Base
                            )
                            recent_data = (
                                session.query(StockModel)
                                .order_by(desc(StockModel.date))
                                .limit(2)
                                .all()
                            )

                            if len(recent_data) == 2:
                                today_price = recent_data[0].close
                                yesterday_price = recent_data[1].close
                                price_diff = today_price - yesterday_price
                                # 差分に応じて表示を調整
                                g.index_data[name] = {
                                    "price": f"{today_price:,.2f}",
                                    "diff": round(price_diff, 2),
                                }
                            else:
                                g.index_data[name] = {
                                    "price": "N/A",
                                    "diff": "N/A",
                                }
                        except Exception as e:
                            # インデックス単位のエラー処理
                            g.index_data[index.get("name", "Unknown")] = {
                                "price": "Error",
                                "diff": "Error",
                            }
                        stock = get_stock_class(db_manager.Base)
                        g.stocks_all = session.query(stock).all()
        except Exception as e:
            g.index_data = {}
            g.stocks_all = []

    # context_processorで年を全テンプレートに提供
    # 全テンプレートに変数を反映
    @app.context_processor
    def inject_data() -> Dict[str, Any]:
        """
        Flaskのコンテキストプロセッサ関数。
        グローバルオブジェクト(g)に格納された 'index_data', 'stocks_all' を
        テンプレートで使用できるように辞書形式で返却する。

        Returns:
            Dict[str, Any]: テンプレートコンテキストに渡す辞書。
            'index_data' キーに対して g.index_data の値を持つ。
            'stocks_all' キーに対して g.stocks_all の値を持つ。
        """

        return {"index_data": g.index_data, "stocks_all": g.stocks_all}

    def get_index_data(code: str) -> Dict[str, List[Any]]:
        """
        指定されたコードに対応する指標データの最新12行を取得し、
        JSON形式（辞書形式）に変換して返す関数。

        Args:
            code (str): 指標データのコード（テーブル名の一部として利用）

        Returns:
            Dict[str, List[Any]]: 各カラム名をキー、各カラムのデータリストを値とする辞書。
                                  データ取得に失敗した場合は空の辞書を返す。
        """
        db_manager = DatabaseManager()

        try:
            # with ステートメントでセッションを管理
            with db_manager.get_session() as session:
                session = db_manager.get_session()
                # データ取得
                StockModel = create_stock_model(f"stock_{code}", db_manager.Base)

                # サブクエリとして定義
                last_12_rows = (
                    session.query(StockModel)
                    .order_by(desc(StockModel.date))  # 降順で取得
                    .limit(12)  # 最新12行を取得
                    .subquery()  # サブクエリ化
                )

                # サブクエリにエイリアスを付ける
                aliased_subquery = aliased(StockModel, last_12_rows)

                # 再度昇順にしてORMモデルとして取得
                stock_datas = (
                    session.query(aliased_subquery)
                    .order_by(aliased_subquery.date.asc())
                    .all()
                )

                # stock_datasの各オブジェクトの属性を辞書形式に変換してリストに格納
                dicts = [vars(stock_data) for stock_data in stock_datas]
                # メタ情報を除いてデータフレームに変換
                df = pd.DataFrame(dicts).drop("_sa_instance_state", axis=1)
                # JSON形式に変換
                json_data = df.to_dict(orient="list")
                return json_data
        except SQLAlchemyError as e:
            # ログやエラーメッセージを記録（ここではプリント）
            print(f"Database error occurred: {e}")
            return pd.DataFrame().to_dict(orient="list")  # エラー時に空のJSONを返す

    # ルートの定義
    @app.route("/")
    def index() -> Response:
        """
        ルート ('/') にアクセスされた際に、株価指数のデータを取得し、
        テンプレートに渡してHTMLをレンダリングするエンドポイント

        Returns:
            Response: レンダリングされたHTMLのレスポンス
        """
        n225_chart_data = get_index_data("N225")
        dji_chart_data = get_index_data("DJI")
        return render_template(
            "index.html", n225_chart_data=n225_chart_data, dji_chart_data=dji_chart_data
        )

    # 銘柄の一覧を表示（READ）
    @app.route("/list")
    def list_view() -> Response:
        """
        株式銘柄の一覧ページをレンダリングするエンドポイント。

        - データベースから株式銘柄をページ単位で取得し、テンプレートに渡す。
        - クエリパラメータからページ番号を取得し、1ページあたりの表示件数は固定（10件）。
        - ページング処理として、総件数と総ページ数を計算してテンプレートに渡す。

        Returns:
            Response: 'list.html' テンプレートをレンダリングしたHTTPレスポンス
        """
        db_manager = DatabaseManager()
        page = int(request.args.get("page", 1))
        per_page = 10  # 1ページあたりのアイテム数

        with db_manager.get_session() as session:
            stock = get_stock_class(db_manager.Base)

            # ページング処理
            total = session.query(stock).count()  # 総アイテム数
            stocks = (
                session.query(stock).offset((page - 1) * per_page).limit(per_page).all()
            )

            total_pages = (total + per_page - 1) // per_page  # 総ページ数を計算

        # テンプレートにデータとページ情報を渡す
        return render_template(
            "list.html",
            stocks=stocks,
            page=page,
            total_pages=total_pages,
        )

    # 新しい銘柄を作成（CREATE）
    @app.route("/create", methods=["GET", "POST"])
    def create() -> Response:
        """
        株式銘柄の作成エンドポイント。

        GETリクエストの場合は、株式作成フォームを表示する。
        POSTリクエストの場合は、フォームから送信されたデータのバリデーションを行い、
        問題なければデータベースに新しい株式銘柄を追加し、一覧ページへリダイレクトする。
        エラーが発生した場合は、エラーメッセージを表示してフォームを再描画する。

        Returns:
            Response: テンプレートのレンダリング結果またはリダイレクト先のレスポンス
        """
        db_manager = DatabaseManager()
        stock_class = get_stock_class(db_manager.Base)  # 必要なモデルを取得

        if request.method == "POST":
            code = request.form.get("code")
            name = request.form.get("name")
            memo = request.form.get("memo")

            errors = []
            # バリデーション: nameが空の場合
            if not name:
                errors.append("銘柄名を入力してください")
            # バリデーション: codeが空の場合
            if not code:
                errors.append("銘柄コードを入力してください")
            # エラーがある場合、フラッシュメッセージを表示してフォームを再描画
            if errors:
                for error in errors:
                    flash(error, "danger")
                return render_template("create.html", errors=errors)

            # データを追加
            with db_manager.get_session() as session:
                try:
                    new_stock = stock_class(code=code, name=name, memo=memo)
                    session.add(new_stock)
                    session.commit()
                    flash("Stock added successfully!", "success")
                    return redirect(url_for("list_view"))
                except Exception as e:
                    session.rollback()  # エラー時はロールバック
                    flash(f"Error occurred: {e}", "danger")
                    return render_template("create.html", errors=[str(e)])

        return render_template("create.html")

    # 銘柄を更新（UPDATE）
    @app.route("/update/<int:id>", methods=["GET", "POST"])
    def update(id) -> Response:
        """
        指定されたIDの銘柄を更新するエンドポイント。

        GETリクエストの場合、指定したIDに対応する株式データをテンプレートに渡して表示する。
        POSTリクエストの場合、フォームから送信されたデータをもとに銘柄情報の更新を行い、
        バリデーションエラーがあればフラッシュメッセージを表示し、エラーがなければ更新後一覧ページにリダイレクトする。

        Args:
            id (int): 更新対象の株式銘柄の識別子

        Returns:
            Response: テンプレートレンダリング結果またはリダイレクト先のHTTPレスポンス
        """
        db_manager = DatabaseManager()
        stock_class = get_stock_class(db_manager.Base)

        with db_manager.get_session() as session:
            # 更新対象のStockを取得
            stock = session.query(stock_class).where(stock_class.id == id).one()

            if request.method == "POST":
                code = request.form["code"]
                name = request.form["name"]
                memo = request.form["memo"]

                errors = []
                # バリデーション: nameが空の場合
                if not name:
                    errors.append("銘柄名を入力してください")
                # バリデーション: codeが空の場合
                if not code:
                    errors.append("銘柄コードを入力してください")
                # エラーがある場合、フラッシュメッセージを表示して再描画
                if errors:
                    for error in errors:
                        flash(error, "danger")
                    return redirect(url_for("update", id=id))

                try:
                    # データの更新
                    stock.code = code
                    stock.name = name
                    stock.memo = memo
                    session.commit()
                    flash("Stock updated successfully!", "success")
                    return redirect(url_for("list_view"))
                except Exception as e:
                    session.rollback()  # エラー時はロールバック
                    flash(f"Error occurred: {e}", "danger")
                    return redirect(url_for("update", id=id))

            return render_template("update.html", stock=stock)

    # 銘柄を削除（DELETE）
    @app.route("/delete/<int:id>", methods=["POST"])
    def delete(id) -> Response:
        """
        指定されたIDの銘柄を削除するエンドポイント。

        GETリクエストの場合、対象の株式データを取得して更新フォームを表示する。
        POSTリクエストの場合、フォームから送信されたデータのバリデーションを行い、更新処理を実行。
        エラーが発生した場合はエラーメッセージをフラッシュし、再度更新フォームへリダイレクトする。

        Args:
            id (int): 更新対象の株式銘柄の識別子

        Returns:
            Response: テンプレートのレンダリング結果またはリダイレクト先のHTTPレスポンス
        """
        db_manager = DatabaseManager()
        stock_class = get_stock_class(db_manager.Base)

        # セッションを with ステートメントで管理
        with db_manager.get_session() as session:
            try:
                # 削除対象のStockを取得
                stock = session.query(stock_class).where(stock_class.id == id).one()
                session.delete(stock)  # 削除
                session.commit()
                flash("Stock deleted successfully!", "success")
            except Exception as e:
                session.rollback()
                flash(f"Error occurred: {e}", "danger")
            return redirect(url_for("list_view"))

    # 銘柄詳細
    @app.route("/stock/<string:code>")
    def stock_detail(code) -> Response:
        """
        指定された株式コードに対応する詳細情報ページをレンダリングするエンドポイント。

        この関数は、以下の処理を行う:
          - yfinanceライブラリを使用して、対象銘柄の財務情報（四半期ごとのデータ）を取得
          - 売上高、営業利益、当期純利益、売上総利益率を抽出し、JSON形式に変換
          - 銘柄情報からPER、PBR、配当利回りを計算（取得できなければ"N/A"とする）
          - データベースから株価チャート用のデータを取得し、DataFrame経由で辞書形式に変換
          - 取得した各種データをテンプレートに渡してレンダリング

        Args:
            code (str): 対象の株式コード（例："7203"）※yfinanceでは証券コードの末尾に".T"を付与して利用

        Returns:
            Response: 'stock_detail.html' テンプレートに各種データを渡してレンダリングしたHTTPレスポンス
        """
        # 銘柄データを取得
        stock_data = yf.Ticker(f"{code}.T")

        # 四半期ごとの財務データを取得
        quarterly_financials = stock_data.quarterly_financials

        try:
            # 必要なデータをリスト形式に変換（NaN除外済み）
            # 売上高
            revenue = series_to_json_format(
                quarterly_financials.loc["Total Revenue"].dropna()
            )

            # 営業利益
            operating_income = series_to_json_format(
                quarterly_financials.loc["Operating Income"].dropna()
            )

            # 当期純利益
            net_income = series_to_json_format(
                quarterly_financials.loc["Net Income"].dropna()
            )

            # 売上総利益率

            gross_profit_margin = series_to_json_format(
                (
                    (
                        quarterly_financials.loc["Gross Profit"]
                        / quarterly_financials.loc["Total Revenue"]
                    )
                    * 100
                ).dropna()
            )
        except KeyError:
            revenue = {}
            operating_income = {}
            net_income = {}
            gross_profit_margin = "N/A"

        # 銘柄情報
        stock_info = stock_data.info

        # PER（株価収益率）／PBR（株価純資産倍率）／配当利回り
        per = "N/A"
        pbr = "N/A"
        dividend_yield = "N/A"

        try:
            # PER（株価収益率）
            per = round(stock_info.get("trailingPE", "N/A"), 2)
            # PBR（株価純資産倍率）
            pbr = round(stock_info.get("priceToBook", "N/A"), 2)
            # 配当利回り（％に変換）
            dividend_yield = round(stock_info.get("dividendYield", "N/A"), 2)
        except Exception as e:
            print(e)

        # データベース関連処理を with ステートメントで管理
        db_manager = DatabaseManager()
        with db_manager.get_session() as session:
            # 銘柄情報
            stock_class = get_stock_class(db_manager.Base)
            stock = session.query(stock_class).where(stock_class.code == code).one()

            # 株価データ取得
            df = get_dataframe(code)
            # データフレームを辞書形式に変換し、テンプレートに渡せる形式に
            chart_data = df.to_dict(orient="list")

            # 直近3日間で上がるパターンに合致しているか
            is_rise = check_rise(df)

        # テンプレートにデータを渡す
        return render_template(
            "stock_detail.html",
            stock=stock,
            revenue=revenue,
            operating_income=operating_income,
            net_income=net_income,
            gross_profit_margin=gross_profit_margin,
            per=per,
            pbr=pbr,
            dividend_yield=dividend_yield,
            chart_data=chart_data,
            is_rise=is_rise,
        )

    @app.route("/update_stock_memo/<string:code>", methods=["PUT"])
    def update_stock_memo(code) -> Response:
        """
        指定された株式銘柄に対応するメモを更新するエンドポイント。

        このエンドポイントは、PUTメソッドでリクエストを受け取り、JSON形式で送信された
        データから新しいメモの内容を取得します。該当する株式銘柄が存在すれば、そのメモを更新し、
        成功メッセージを返す。必要なデータが存在しない場合や、該当するデータが見つからない場合、
        エラーレスポンスを返す。

        Args:
            code (int): 更新対象の株式銘柄のコード（URLパラメータとして渡される）

        Returns:
            Response: JSON形式のレスポンスとHTTPステータスコードを含むレスポンスオブジェクト
        """
        db_manager = DatabaseManager()
        stock_class = get_stock_class(db_manager.Base)

        try:
            # リクエストデータをJSON形式で取得
            data = request.get_json()
            new_memo = data.get("content")  # 'content' が新しいメモの内容

            # 必要なフィールドが送信されていない場合のエラーハンドリング
            if new_memo is None:
                return jsonify({"error": "Content is required"}), 400

            # セッションを使用してデータを更新
            with db_manager.get_session() as session:
                # 該当する stock を検索
                stock = (
                    session.query(stock_class).filter(stock_class.code == code).first()
                )

                # 該当する stock が存在しない場合のエラーハンドリング
                if not stock:
                    return jsonify({"error": "Stock not found"}), 404

                # memo を更新
                stock.memo = new_memo
                session.commit()  # 更新内容を保存

            # 成功レスポンスを返す
            return jsonify(
                {"message": "Stock memo updated successfully", "code": code}
            ), 200

        except Exception as e:
            # エラー発生時のレスポンス
            return jsonify({"error": str(e)}), 500

    # トレンドラインの更新　非同期通信で利用
    @app.route("/update_trendlines/<string:code>")
    def update_trendlines(code):
        # 各値を取得
        lookback = int(request.args.get("lookback", 100))
        min_touches = int(request.args.get("min_touches", 5))
        order = int(request.args.get("order", 5))

        # 株価データ取得
        df = get_dataframe(code)

        # トレンドラインを算出
        trend_lines = detect_trend_lines(df, lookback, min_touches, order)

        return jsonify(trend_lines)

    return app
