from __future__ import annotations

import argparse
import base64
import hashlib
import hmac
import json
import logging
import os
import threading
import time
import traceback
from dataclasses import asdict, is_dataclass
from http import HTTPStatus
from http.cookies import SimpleCookie
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from typing import Any
from urllib.parse import unquote, urlparse

from kraken_bot.config import BotConfig, load_config
from kraken_bot.engine import PairRunResult, TradingBot
from kraken_bot.models import Side, Signal
from kraken_bot.strategies import STRATEGY_REGISTRY
from kraken_bot.web.ledger import TradeLedger

LOGGER = logging.getLogger("kraken_bot.web")
STATIC_DIR = Path(__file__).with_name("static")


class BotRuntime:
    def __init__(self, config_path: Path) -> None:
        self.config_path = config_path
        self.credentials_path = config_path.parent / "state" / "kraken_credentials.json"
        self.ledger_path = config_path.parent / "state" / "trade_ledger.json"
        self.runtime_state_path = config_path.parent / "state" / "runtime_state.json"
        self.lock = threading.RLock()
        self.history: list[dict[str, Any]] = []
        self.loop_thread: threading.Thread | None = None
        self.stop_event = threading.Event()
        self.loop_sleep_seconds = 60
        self.last_error: str | None = None
        self.service_enabled = self._read_runtime_state().get("service_enabled", True)
        self.reload()

    def reload(self) -> None:
        with self.lock:
            self.config = load_config(self.config_path)
            self._apply_saved_credentials(self.config)
            self.bot = TradingBot(self.config)
            self.ledger = TradeLedger(self.ledger_path)

    def snapshot(self) -> dict[str, Any]:
        with self.lock:
            loop_running = bool(self.loop_thread and self.loop_thread.is_alive())
            return {
                "config_path": str(self.config_path),
                "dry_run": self.config.risk.dry_run,
                "quote_currency": self.config.risk.quote_currency,
                "fixed_order_quote": self.config.risk.fixed_order_quote,
                "max_order_quote": self.config.risk.max_order_quote,
                "cooldown_seconds": self.config.risk.cooldown_seconds,
                "pairs": [asdict(pair) for pair in self.config.pairs],
                "strategies": sorted(STRATEGY_REGISTRY.keys()),
                "history": self.history[-100:],
                "loop": {
                    "running": loop_running,
                    "sleep_seconds": self.loop_sleep_seconds,
                },
                "last_error": self.last_error,
                "credentials": self.credentials_status(),
                "open_positions": self.ledger.positions,
                "bot_status": {
                    "service": "running" if self.service_enabled else "stopped",
                    "loop_running": loop_running,
                    "mode": "dry_run" if self.config.risk.dry_run else "live",
                    **self.ledger.status_summary(),
                },
            }

    def credentials_status(self) -> dict[str, Any]:
        saved = self._read_credentials()
        key = saved.get("api_key") or os.getenv(self.config.kraken.api_key_env)
        secret = saved.get("api_secret") or os.getenv(self.config.kraken.api_secret_env)
        return {
            "has_api_key": bool(key),
            "has_api_secret": bool(secret),
            "api_key_env": self.config.kraken.api_key_env,
            "api_secret_env": self.config.kraken.api_secret_env,
            "storage": str(self.credentials_path),
        }

    def save_credentials(self, api_key: str, api_secret: str) -> dict[str, Any]:
        api_key = api_key.strip()
        api_secret = api_secret.strip()
        if not api_key or not api_secret:
            raise ValueError("API key and API secret are required")
        self.credentials_path.parent.mkdir(parents=True, exist_ok=True)
        payload = {
            "api_key": api_key,
            "api_secret": api_secret,
            "updated_at": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
        }
        self.credentials_path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
        try:
            self.credentials_path.chmod(0o600)
        except OSError:
            pass
        self.reload()
        return self.credentials_status()

    def _apply_saved_credentials(self, config: BotConfig) -> None:
        saved = self._read_credentials()
        if saved.get("api_key"):
            os.environ[config.kraken.api_key_env] = saved["api_key"]
        if saved.get("api_secret"):
            os.environ[config.kraken.api_secret_env] = saved["api_secret"]

    def _read_credentials(self) -> dict[str, str]:
        if not self.credentials_path.exists():
            return {}
        try:
            data = json.loads(self.credentials_path.read_text(encoding="utf-8"))
        except json.JSONDecodeError:
            return {}
        return {
            "api_key": str(data.get("api_key", "")),
            "api_secret": str(data.get("api_secret", "")),
            "updated_at": str(data.get("updated_at", "")),
        }

    def run_once(self) -> list[dict[str, Any]]:
        with self.lock:
            if not self.service_enabled:
                raise RuntimeError("Bot service is stopped")
            started = time.time()
            results = [serialize_result(result) for result in self.bot.run_once()]
            for result in results:
                self._record_trade_marker(result)
            batch = {
                "time": started,
                "iso_time": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(started)),
                "results": results,
            }
            self.history.append(batch)
            self.history = self.history[-100:]
            self.last_error = None
            return results

    def run_pair(self, symbol: str) -> list[dict[str, Any]]:
        with self.lock:
            if not self.service_enabled:
                raise RuntimeError("Bot service is stopped")
            pairs = {pair.symbol.upper(): pair for pair in self.config.pairs}
            pair = pairs.get(symbol.upper())
            if pair is None:
                raise ValueError(f"Unknown pair '{symbol}'")
            started = time.time()
            result = serialize_result(self.bot.run_pair(pair))
            self._record_trade_marker(result)
            self.history.append(
                {
                    "time": started,
                    "iso_time": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(started)),
                    "results": [result],
                }
            )
            self.history = self.history[-100:]
            self.last_error = None
            return [result]

    def chart_data(self, symbol: str) -> dict[str, Any]:
        with self.lock:
            pairs = {pair.symbol.upper(): pair for pair in self.config.pairs}
            pair = pairs.get(symbol.upper())
            if pair is None:
                raise ValueError(f"Unknown pair '{symbol}'")
            candles = self.bot.client.get_ohlc(pair.symbol, pair.interval, pair.candles)
            return {
                "symbol": pair.symbol,
                "strategy": pair.strategy,
                "interval": pair.interval,
                "candles": [asdict(candle) for candle in candles],
                "markers": self.ledger.for_symbol(pair.symbol),
                "open_position": self.ledger.open_position(pair.symbol),
            }

    def start_loop(self, sleep_seconds: int) -> None:
        with self.lock:
            if not self.service_enabled:
                raise RuntimeError("Bot service is stopped")
            self.loop_sleep_seconds = max(10, int(sleep_seconds))
            if self.loop_thread and self.loop_thread.is_alive():
                return
            self.stop_event.clear()
            self.loop_thread = threading.Thread(target=self._loop_worker, name="phenocrypto-loop", daemon=True)
            self.loop_thread.start()

    def stop_loop(self) -> None:
        self.stop_event.set()

    def start_service(self) -> dict[str, Any]:
        with self.lock:
            self.service_enabled = True
            self._write_runtime_state()
            return {"state": "running", "message": "Bot service enabled"}

    def stop_service(self) -> dict[str, Any]:
        with self.lock:
            self.service_enabled = False
            self.stop_loop()
            self._write_runtime_state()
            return {"state": "stopped", "message": "Bot service stopped"}

    def _read_runtime_state(self) -> dict[str, Any]:
        if not self.runtime_state_path.exists():
            return {}
        try:
            return json.loads(self.runtime_state_path.read_text(encoding="utf-8"))
        except json.JSONDecodeError:
            return {}

    def _write_runtime_state(self) -> None:
        self.runtime_state_path.parent.mkdir(parents=True, exist_ok=True)
        payload = {
            "service_enabled": self.service_enabled,
            "updated_at": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
        }
        self.runtime_state_path.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
        try:
            self.runtime_state_path.chmod(0o600)
        except OSError:
            pass

    def _loop_worker(self) -> None:
        while not self.stop_event.is_set():
            try:
                self.run_once()
            except Exception as exc:
                LOGGER.exception("Loop run failed")
                with self.lock:
                    self.last_error = f"{exc}\n{traceback.format_exc()}"
            self.stop_event.wait(self.loop_sleep_seconds)

    def _record_trade_marker(self, result: dict[str, Any]) -> None:
        signal = result.get("signal", {})
        side_value = signal.get("side", "hold")
        try:
            side = Side(side_value)
        except ValueError:
            return
        self.ledger.record_signal(
            symbol=str(result.get("symbol", "")),
            side=side,
            price=float(result.get("price", 0.0)),
            strategy=str(result.get("strategy", "")),
            reason=str(signal.get("reason", result.get("message", ""))),
            dry_run=self.config.risk.dry_run,
        )


class AppRequestHandler(BaseHTTPRequestHandler):
    runtime: BotRuntime
    base_path: str = "/phenocrypto"
    admin_token: str | None = None
    login_username: str = "admin"
    login_password: str | None = None
    session_secret: str = "phenocrypto-dev-secret"
    session_seconds: int = 12 * 60 * 60
    cookie_name: str = "phenocrypto_session"

    def do_GET(self) -> None:
        route = self._route_path()
        if route == "/login":
            if self._authenticated():
                self._redirect(self._base_url() + "/dashboard")
                return
            self._send_file(STATIC_DIR / "login.html", "text/html; charset=utf-8")
            return
        if route in {"", "/"}:
            if not self._authenticated():
                self._redirect(self._base_url() + "/login")
                return
            self._redirect(self._base_url() + "/dashboard")
            return
        if route == "/dashboard":
            if not self._authenticated():
                self._redirect(self._base_url() + "/login")
                return
            self._send_file(STATIC_DIR / "dashboard.html", "text/html; charset=utf-8")
            return
        if route == "/trading":
            if not self._authenticated():
                self._redirect(self._base_url() + "/login")
                return
            self._send_file(STATIC_DIR / "trading.html", "text/html; charset=utf-8")
            return
        if route == "/settings":
            if not self._authenticated():
                self._redirect(self._base_url() + "/login")
                return
            self._send_file(STATIC_DIR / "settings.html", "text/html; charset=utf-8")
            return
        if route == "/api/session":
            self._send_json({"ok": True, "authenticated": self._authenticated(), "username": self.login_username})
            return
        if route == "/api/status":
            if not self._authorized():
                self._send_json({"ok": False, "error": "Login required"}, HTTPStatus.UNAUTHORIZED)
                return
            self._send_json({"ok": True, **self.runtime.snapshot()})
            return
        if route == "/api/credentials":
            if not self._authorized():
                self._send_json({"ok": False, "error": "Login required"}, HTTPStatus.UNAUTHORIZED)
                return
            self._send_json({"ok": True, "credentials": self.runtime.credentials_status()})
            return
        if route.startswith("/api/chart/"):
            if not self._authorized():
                self._send_json({"ok": False, "error": "Login required"}, HTTPStatus.UNAUTHORIZED)
                return
            symbol = unquote(route.removeprefix("/api/chart/"))
            try:
                self._send_json({"ok": True, "chart": self.runtime.chart_data(symbol)})
            except Exception as exc:
                self._send_json({"ok": False, "error": str(exc)}, HTTPStatus.BAD_REQUEST)
            return
        if route.startswith("/static/"):
            name = route.removeprefix("/static/")
            self._send_static(name)
            return
        self._send_json({"ok": False, "error": "Not found"}, HTTPStatus.NOT_FOUND)

    def do_POST(self) -> None:
        route = self._route_path()
        if route == "/api/login":
            try:
                self._handle_login()
            except Exception as exc:
                self._send_json({"ok": False, "error": str(exc)}, HTTPStatus.BAD_REQUEST)
            return
        if route == "/api/logout":
            self._send_json({"ok": True}, extra_headers=[self._clear_session_cookie_header()])
            return
        if not self._authorized():
            self._send_json({"ok": False, "error": "Login required"}, HTTPStatus.UNAUTHORIZED)
            return
        try:
            if route == "/api/run":
                self._send_json({"ok": True, "results": self.runtime.run_once(), "status": self.runtime.snapshot()})
                return
            if route.startswith("/api/run/"):
                symbol = unquote(route.removeprefix("/api/run/"))
                self._send_json({"ok": True, "results": self.runtime.run_pair(symbol), "status": self.runtime.snapshot()})
                return
            if route == "/api/reload":
                self.runtime.reload()
                self._send_json({"ok": True, "status": self.runtime.snapshot()})
                return
            if route == "/api/credentials":
                payload = self._read_json()
                credentials = self.runtime.save_credentials(
                    str(payload.get("api_key", "")),
                    str(payload.get("api_secret", "")),
                )
                self._send_json({"ok": True, "credentials": credentials, "status": self.runtime.snapshot()})
                return
            if route == "/api/loop/start":
                payload = self._read_json()
                self.runtime.start_loop(int(payload.get("sleep_seconds", 60)))
                self._send_json({"ok": True, "status": self.runtime.snapshot()})
                return
            if route == "/api/loop/stop":
                self.runtime.stop_loop()
                self._send_json({"ok": True, "status": self.runtime.snapshot()})
                return
            if route == "/api/service/start":
                result = self.runtime.start_service()
                self._send_json({"ok": True, "service": result, "status": self.runtime.snapshot()})
                return
            if route == "/api/service/stop":
                result = self.runtime.stop_service()
                self._send_json({"ok": True, "service": result, "status": self.runtime.snapshot()})
                return
        except Exception as exc:
            LOGGER.exception("Request failed")
            self._send_json({"ok": False, "error": str(exc)}, HTTPStatus.BAD_REQUEST)
            return
        self._send_json({"ok": False, "error": "Not found"}, HTTPStatus.NOT_FOUND)

    def log_message(self, format: str, *args: Any) -> None:
        LOGGER.info("%s - %s", self.address_string(), format % args)

    def _route_path(self) -> str:
        parsed = urlparse(self.path)
        path = parsed.path.rstrip("/") or "/"
        base = self.base_path.rstrip("/")
        if base and path == base:
            return "/"
        if base and path.startswith(base + "/"):
            return path[len(base) :]
        return path

    def _base_url(self) -> str:
        return self.base_path.rstrip("/") if self.base_path else ""

    def _read_json(self) -> dict[str, Any]:
        length = int(self.headers.get("Content-Length", "0"))
        if length <= 0:
            return {}
        raw = self.rfile.read(length).decode("utf-8")
        if not raw:
            return {}
        try:
            return json.loads(raw)
        except json.JSONDecodeError as exc:
            raise ValueError("Request body must be valid JSON") from exc

    def _authorized(self) -> bool:
        if self._authenticated():
            return True
        if not self.admin_token:
            return False
        header = self.headers.get("X-Admin-Token", "")
        bearer = self.headers.get("Authorization", "")
        if bearer.startswith("Bearer "):
            bearer = bearer.removeprefix("Bearer ").strip()
        return header == self.admin_token or bearer == self.admin_token

    def _authenticated(self) -> bool:
        cookie_header = self.headers.get("Cookie", "")
        if not cookie_header:
            return False
        cookies = SimpleCookie()
        cookies.load(cookie_header)
        morsel = cookies.get(self.cookie_name)
        if morsel is None:
            return False
        return self._verify_session(morsel.value)

    def _handle_login(self) -> None:
        payload = self._read_json()
        username = str(payload.get("username", ""))
        password = str(payload.get("password", ""))
        if username != self.login_username or not self.login_password or password != self.login_password:
            self._send_json({"ok": False, "error": "Invalid username or password"}, HTTPStatus.UNAUTHORIZED)
            return
        cookie_value = self._make_session(username)
        self._send_json(
            {"ok": True, "username": username},
            extra_headers=[self._session_cookie_header(cookie_value)],
        )

    def _make_session(self, username: str) -> str:
        expires = int(time.time()) + self.session_seconds
        payload = f"{username}|{expires}"
        signature = hmac.new(self.session_secret.encode(), payload.encode(), hashlib.sha256).hexdigest()
        raw = f"{payload}|{signature}".encode()
        return base64.urlsafe_b64encode(raw).decode()

    def _verify_session(self, value: str) -> bool:
        try:
            raw = base64.urlsafe_b64decode(value.encode()).decode()
            username, expires_raw, signature = raw.split("|", 2)
            payload = f"{username}|{expires_raw}"
            expected = hmac.new(self.session_secret.encode(), payload.encode(), hashlib.sha256).hexdigest()
            return (
                username == self.login_username
                and int(expires_raw) >= int(time.time())
                and hmac.compare_digest(signature, expected)
            )
        except Exception:
            return False

    def _session_cookie_header(self, value: str) -> tuple[str, str]:
        return (
            "Set-Cookie",
            (
                f"{self.cookie_name}={value}; Path={self.base_path or '/'}; "
                f"Max-Age={self.session_seconds}; HttpOnly; SameSite=Lax"
            ),
        )

    def _clear_session_cookie_header(self) -> tuple[str, str]:
        return (
            "Set-Cookie",
            f"{self.cookie_name}=; Path={self.base_path or '/'}; Max-Age=0; HttpOnly; SameSite=Lax",
        )

    def _redirect(self, location: str) -> None:
        self.send_response(HTTPStatus.FOUND)
        self.send_header("Location", location)
        self.send_header("Cache-Control", "no-store, max-age=0")
        self.end_headers()

    def _send_static(self, name: str) -> None:
        safe_name = name.replace("\\", "/").lstrip("/")
        if ".." in safe_name.split("/"):
            self._send_json({"ok": False, "error": "Invalid path"}, HTTPStatus.BAD_REQUEST)
            return
        path = STATIC_DIR / safe_name
        content_type = "application/octet-stream"
        if path.suffix == ".css":
            content_type = "text/css; charset=utf-8"
        elif path.suffix == ".js":
            content_type = "application/javascript; charset=utf-8"
        elif path.suffix == ".html":
            content_type = "text/html; charset=utf-8"
        self._send_file(path, content_type)

    def _send_file(self, path: Path, content_type: str) -> None:
        if not path.exists() or not path.is_file():
            self._send_json({"ok": False, "error": "Not found"}, HTTPStatus.NOT_FOUND)
            return
        data = path.read_bytes()
        self.send_response(HTTPStatus.OK)
        self.send_header("Content-Type", content_type)
        self.send_header("Content-Length", str(len(data)))
        self.send_header("Cache-Control", "no-store")
        self.end_headers()
        self.wfile.write(data)

    def _send_json(
        self,
        payload: dict[str, Any],
        status: HTTPStatus = HTTPStatus.OK,
        extra_headers: list[tuple[str, str]] | None = None,
    ) -> None:
        data = json.dumps(payload, default=json_default).encode("utf-8")
        self.send_response(status)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", str(len(data)))
        self.send_header("Cache-Control", "no-store")
        for key, value in extra_headers or []:
            self.send_header(key, value)
        self.end_headers()
        self.wfile.write(data)


def json_default(value: Any) -> Any:
    if is_dataclass(value):
        return asdict(value)
    if hasattr(value, "value"):
        return value.value
    if isinstance(value, Path):
        return str(value)
    raise TypeError(f"Object of type {type(value).__name__} is not JSON serializable")


def serialize_signal(signal: Signal) -> dict[str, Any]:
    return {
        "side": signal.side.value,
        "reason": signal.reason,
        "confidence": signal.confidence,
        "metadata": signal.metadata or {},
    }


def serialize_result(result: PairRunResult) -> dict[str, Any]:
    return {
        "symbol": result.symbol,
        "strategy": result.strategy,
        "signal": serialize_signal(result.signal),
        "price": result.price,
        "executed": result.executed,
        "message": result.message,
    }


def build_server(
    config_path: Path,
    host: str,
    port: int,
    base_path: str,
    admin_token: str | None,
    login_username: str,
    login_password: str | None,
    session_secret: str,
) -> ThreadingHTTPServer:
    runtime = BotRuntime(config_path)
    handler = type(
        "PhenocryptoRequestHandler",
        (AppRequestHandler,),
        {
            "runtime": runtime,
            "base_path": base_path.rstrip("/") or "",
            "admin_token": admin_token,
            "login_username": login_username,
            "login_password": login_password,
            "session_secret": session_secret,
        },
    )
    return ThreadingHTTPServer((host, port), handler)


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Run the Phenocrypto web dashboard.")
    parser.add_argument("--config", default="config.example.json")
    parser.add_argument("--host", default=os.getenv("PHENOCRYPTO_HOST", "127.0.0.1"))
    parser.add_argument("--port", type=int, default=int(os.getenv("PHENOCRYPTO_PORT", "3105")))
    parser.add_argument("--base-path", default=os.getenv("PHENOCRYPTO_BASE_PATH", "/phenocrypto"))
    parser.add_argument("--admin-token", default=os.getenv("PHENOCRYPTO_ADMIN_TOKEN"))
    parser.add_argument("--login-username", default=os.getenv("PHENOCRYPTO_USERNAME", "admin"))
    parser.add_argument("--login-password", default=os.getenv("PHENOCRYPTO_PASSWORD"))
    parser.add_argument(
        "--session-secret",
        default=os.getenv("PHENOCRYPTO_SESSION_SECRET", os.getenv("PHENOCRYPTO_ADMIN_TOKEN", "phenocrypto-dev-secret")),
    )
    parser.add_argument("--log-level", default=os.getenv("PHENOCRYPTO_LOG_LEVEL", "INFO"))
    return parser.parse_args()


def main() -> int:
    args = parse_args()
    logging.basicConfig(
        level=getattr(logging, args.log_level.upper(), logging.INFO),
        format="%(asctime)s %(levelname)s %(name)s: %(message)s",
    )
    server = build_server(
        Path(args.config),
        args.host,
        args.port,
        args.base_path,
        args.admin_token,
        args.login_username,
        args.login_password,
        args.session_secret,
    )
    LOGGER.info("Phenocrypto dashboard listening on http://%s:%s%s", args.host, args.port, args.base_path)
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        LOGGER.info("Shutting down")
    finally:
        server.server_close()
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
