from __future__ import annotations

from datetime import datetime, timedelta, timezone
from functools import wraps
from typing import Any, Callable

from flask import Flask, abort, jsonify, redirect, request

from .config import Settings, load_settings
from .patreon_client import PatreonClient
from .storage import Storage


def create_app() -> Flask:
    settings = load_settings()
    app = Flask(__name__)
    app.config["SECRET_KEY"] = settings.flask_secret_key

    storage = Storage(settings.database_path)
    patreon = PatreonClient(settings)

    def require_game_api_key(view: Callable[..., Any]) -> Callable[..., Any]:
        @wraps(view)
        def wrapped(*args: Any, **kwargs: Any) -> Any:
            if request.headers.get("X-Game-Api-Key") != settings.game_api_key:
                return jsonify({"error": "unauthorized"}), 401
            return view(*args, **kwargs)

        return wrapped

    def maybe_refresh_player_link(player_link: dict[str, Any]) -> dict[str, Any]:
        expires_at = datetime.fromisoformat(player_link["token_expires_at"])
        if expires_at - datetime.now(timezone.utc) > timedelta(minutes=5):
            return player_link

        refreshed = patreon.refresh_token(player_link["refresh_token"])
        entitlement = patreon.fetch_identity_and_membership(refreshed["access_token"])
        storage.upsert_player_link(
            player_id=player_link["player_id"],
            patreon_user_id=entitlement["patreon_user_id"],
            access_token=refreshed["access_token"],
            refresh_token=refreshed["refresh_token"],
            token_expires_at=patreon.token_expires_at(refreshed["expires_in"]),
            scope=refreshed.get("scope", ""),
            entitlement=entitlement,
        )
        updated = storage.get_player_link(player_link["player_id"])
        if not updated:
            raise RuntimeError("Player link disappeared during refresh")
        return updated

    @app.get("/health")
    def health() -> Any:
        return jsonify({"ok": True})

    @app.post("/api/link-sessions")
    @require_game_api_key
    def create_link_session() -> Any:
        data = request.get_json(silent=True) or {}
        player_id = str(data.get("player_id", "")).strip()
        if not player_id:
            return jsonify({"error": "player_id is required"}), 400

        session = storage.create_link_session(player_id)
        auth_url = patreon.build_authorize_url(session["state_token"])
        return (
            jsonify(
                {
                    "player_id": player_id,
                    "link_token": session["link_token"],
                    "auth_url": auth_url,
                    "status_url": f"{settings.public_base_url}/api/link-sessions/{session['link_token']}",
                }
            ),
            201,
        )

    @app.get("/api/link-sessions/<link_token>")
    @require_game_api_key
    def get_link_session(link_token: str) -> Any:
        session = storage.get_link_session_by_token(link_token)
        if not session:
            return jsonify({"error": "not_found"}), 404

        payload: dict[str, Any] = {
            "player_id": session["player_id"],
            "status": session["status"],
            "created_at": session["created_at"],
            "updated_at": session["updated_at"],
            "completed_at": session["completed_at"],
            "error_message": session["error_message"],
        }
        if session["status"] == "linked":
            player_link = storage.get_player_link(session["player_id"])
            if player_link:
                payload["entitlement"] = player_link["entitlement"]

        return jsonify(payload)

    @app.get("/api/players/<player_id>/entitlement")
    @require_game_api_key
    def get_entitlement(player_id: str) -> Any:
        player_link = storage.get_player_link(player_id)
        if not player_link:
            return jsonify({"linked": False, "entitlement": None})

        player_link["player_id"] = player_id
        player_link = maybe_refresh_player_link(player_link)
        return jsonify({"linked": True, "entitlement": player_link["entitlement"]})

    @app.get("/auth/patreon/start")
    def auth_start() -> Any:
        link_token = request.args.get("link_token", "").strip()
        if not link_token:
            abort(400)

        session = storage.get_link_session_by_token(link_token)
        if not session:
            abort(404)

        auth_url = patreon.build_authorize_url(session["state_token"])
        return redirect(auth_url, code=302)

    @app.get("/auth/patreon/callback")
    def auth_callback() -> Any:
        state = request.args.get("state", "").strip()
        code = request.args.get("code", "").strip()
        error = request.args.get("error", "").strip()

        session = storage.get_link_session_by_state(state)
        if not session:
            return "Unknown or expired Patreon login state.", 400

        if error:
            storage.update_link_session_status(
                session["link_token"],
                status="error",
                error_message=error,
            )
            return f"Patreon authorization failed: {error}", 400

        if not code:
            storage.update_link_session_status(
                session["link_token"],
                status="error",
                error_message="Missing OAuth code",
            )
            return "Patreon authorization failed: missing OAuth code.", 400

        try:
            tokens = patreon.exchange_code(code)
            entitlement = patreon.fetch_identity_and_membership(tokens["access_token"])
            storage.upsert_player_link(
                player_id=session["player_id"],
                patreon_user_id=entitlement["patreon_user_id"],
                access_token=tokens["access_token"],
                refresh_token=tokens["refresh_token"],
                token_expires_at=patreon.token_expires_at(tokens["expires_in"]),
                scope=tokens.get("scope", ""),
                entitlement=entitlement,
            )
            storage.update_link_session_status(
                session["link_token"],
                status="linked",
                completed=True,
            )
        except Exception as exc:  # pragma: no cover - defensive failure path
            storage.update_link_session_status(
                session["link_token"],
                status="error",
                error_message=str(exc),
            )
            return (
                "Patreon authorization completed, but entitlement sync failed. "
                "Check the backend logs for details.",
                500,
            )

        return (
            "Patreon account linked successfully. You can return to the game; it can now poll "
            "the backend for your entitlement.",
            200,
        )

    return app


app = create_app()
