from __future__ import annotations

import secrets
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import date, datetime
from typing import Any
from zoneinfo import ZoneInfo

from backend.data.gateway import DataGateway
from backend.database.connection import Database
from backend.features.accounts.models import Principal
from backend.features.review.prompt import PROMPT_VERSION, messages
from backend.features.review.repository import ReviewRepository
from backend.features.review.views import alert as alert_view
from backend.features.review.views import note as note_view
from backend.features.review.views import trade_summary, trades
from backend.features.screener.service import ScreenerService
from backend.llm.gateway import LLMCall, LLMGateway, LLMGatewayError

SHANGHAI = ZoneInfo("Asia/Shanghai")


class ReviewError(RuntimeError):
    pass


@dataclass(frozen=True, slots=True)
class PreparedAssistant:
    call: LLMCall
    prompt: list[dict[str, str]]
    context_date: str


class ReviewService:
    def __init__(
        self,
        database: Database,
        repository: ReviewRepository,
        gateway: DataGateway,
        screener: ScreenerService,
        llm: LLMGateway,
    ) -> None:
        self._database = database
        self._repository = repository
        self._gateway = gateway
        self._screener = screener
        self._llm = llm

    def workspace(self, principal: Principal, requested_date: str) -> dict[str, Any]:
        trade_date = self._trade_date(requested_date)
        with self._database.read() as connection:
            watches = self._watch_rows(connection, principal.user.id, trade_date)
            daily = self._repository.note(connection, principal.user.id, "", trade_date)
            history = self._repository.notes(connection, principal.user.id, "", 60)
            trade_rows = trades(self._repository.trades(connection, principal.user.id))
        return {
            "trade_date": trade_date,
            "watchlist": watches,
            "daily": note_view(daily),
            "history": [note_view(row) for row in history],
            "trades": trade_rows,
            "trade_summary": trade_summary(trade_rows),
        }

    def add_watch(self, principal: Principal, identifier: str) -> dict[str, Any]:
        normalized = identifier.strip().upper()
        candidates = self._gateway.search(normalized)
        item = next(
            (
                candidate
                for candidate in candidates
                if candidate.entity_type == "stock" and candidate.identifier == normalized
            ),
            None,
        )
        if item is None:
            raise ReviewError("未找到可加入自选的股票。")
        with self._database.transaction() as connection:
            self._repository.save_watch(
                connection,
                principal.user.id,
                {"identifier": item.identifier, "name": item.name, "sector": item.sector},
                _now(),
            )
        return {"identifier": item.identifier, "name": item.name}

    def save_watch_remark(self, principal: Principal, identifier: str, remark: str) -> None:
        normalized = _text(remark, 500)
        with self._database.transaction() as connection:
            if not self._repository.save_watch_remark(
                connection, principal.user.id, identifier, normalized
            ):
                raise ReviewError("自选记录不存在。")

    def delete_watch(self, principal: Principal, identifier: str) -> None:
        with self._database.transaction() as connection:
            if not self._repository.delete_watch(connection, principal.user.id, identifier):
                raise ReviewError("自选记录不存在。")

    def save_note(self, principal: Principal, payload: dict[str, Any]) -> int:
        data = {
            "trade_date": _date(payload["trade_date"]),
            "code": payload.get("code", "").strip().upper(),
            "stock_name": _text(payload.get("stock_name", ""), 40),
            "summary": _text(payload.get("summary", ""), 500),
            "content": _text(payload.get("content", ""), 5000),
            "plan": _text(payload.get("plan", ""), 2000),
        }
        if data["code"] and not data["stock_name"]:
            raise ReviewError("个股名称不能为空。")
        with self._database.transaction() as connection:
            return self._repository.save_note(connection, principal.user.id, data, _now())

    def stock_notes(self, principal: Principal, code: str) -> list[dict[str, Any]]:
        with self._database.read() as connection:
            return [
                note_view(row)
                for row in self._repository.notes(
                    connection, principal.user.id, code.strip().upper()
                )
            ]

    def delete_note(self, principal: Principal, note_id: int) -> None:
        with self._database.transaction() as connection:
            if not self._repository.delete_note(connection, principal.user.id, note_id):
                raise ReviewError("复盘记录不存在。")

    def save_trade(self, principal: Principal, payload: dict[str, Any]) -> int:
        data = dict(payload)
        data["trade_date"] = _date(str(data["trade_date"]))
        data["code"] = str(data["code"]).strip().upper()
        data["name"] = _text(data["name"], 40, True)
        data["thesis"] = _text(data.get("thesis", ""), 2000)
        data["execution"] = _text(data.get("execution", ""), 2000)
        data["tags"] = [_text(item, 20, True) for item in data.get("tags", [])][:8]
        with self._database.transaction() as connection:
            return self._repository.save_trade(connection, principal.user.id, data, _now())

    def delete_trade(self, principal: Principal, trade_id: int) -> None:
        with self._database.transaction() as connection:
            if not self._repository.delete_trade(connection, principal.user.id, trade_id):
                raise ReviewError("交易记录不存在。")

    def alert_center(self, principal: Principal, unread: bool = False) -> dict[str, Any]:
        self._sync_tracking_alerts(principal.user.id)
        today = date.today().isoformat()
        with self._database.read() as connection:
            rows = self._repository.alerts(connection, principal.user.id, unread)
            items = [alert_view(row, today) for row in rows]
            if unread:
                items = [item for item in items if item["due"]]
            count = self._repository.unread_count(connection, principal.user.id, today)
        return {"items": items, "unread_count": count, "as_of": today}

    def create_alert(self, principal: Principal, payload: dict[str, Any]) -> int:
        data = {
            "kind": "manual",
            "title": _text(payload["title"], 80, True),
            "content": _text(payload.get("content", ""), 500),
            "available_date": _date(payload["remind_date"]),
            "code": _text(payload.get("code", ""), 12),
            "dedupe_key": f"manual:{secrets.token_hex(12)}",
        }
        with self._database.transaction() as connection:
            return self._repository.save_alert(connection, principal.user.id, data, _now())

    def mark_alert(self, principal: Principal, alert_id: int) -> None:
        with self._database.transaction() as connection:
            if not self._repository.mark_alert(connection, principal.user.id, alert_id, _now()):
                raise ReviewError("提醒不存在。")

    def mark_all_alerts(self, principal: Principal) -> int:
        with self._database.transaction() as connection:
            return self._repository.mark_all_alerts(
                connection, principal.user.id, date.today().isoformat(), _now()
            )

    def delete_alert(self, principal: Principal, alert_id: int) -> None:
        with self._database.transaction() as connection:
            if not self._repository.delete_alert(connection, principal.user.id, alert_id):
                raise ReviewError("提醒不存在。")

    def messages(self, principal: Principal) -> list[dict[str, Any]]:
        with self._database.read() as connection:
            return [dict(row) for row in self._repository.messages(connection, principal.user.id)]

    def clear_messages(self, principal: Principal) -> int:
        with self._database.transaction() as connection:
            return self._repository.clear_messages(connection, principal.user.id)

    def prepare_assistant(
        self, principal: Principal, requested_date: str, question: str
    ) -> PreparedAssistant:
        context_date = self._trade_date(requested_date)
        normalized = _text(question, 2000, True)
        context = self._assistant_context(principal, context_date)
        history = self._history(principal.user.id)
        prompt = messages(context, history, normalized)
        call = self._llm.prepare(
            principal,
            feature="review_assistant",
            prompt_version=PROMPT_VERSION,
            business_id=context_date,
            input_chars=sum(len(item["content"]) for item in prompt),
        )
        with self._database.transaction() as connection:
            self._repository.add_message(
                connection,
                principal.user.id,
                {
                    "role": "user",
                    "content": normalized,
                    "context_date": context_date,
                    "request_id": call.request_id,
                    "status": "complete",
                },
                _now(),
            )
        return PreparedAssistant(call, prompt, context_date)

    def stream_assistant(self, prepared: PreparedAssistant) -> Iterator[dict[str, Any]]:
        answer = ""
        stream = self._llm.stream(prepared.call, prepared.prompt)
        try:
            for event in stream:
                if event.type == "delta":
                    answer += event.content
                    yield {
                        "type": "delta",
                        "content": event.content,
                        "request_id": event.request_id,
                    }
                elif event.type == "done":
                    self._save_assistant(prepared, answer, "complete")
                    yield {"type": "done", "request_id": event.request_id}
        except GeneratorExit:
            stream.close()
            if answer:
                self._save_assistant(prepared, answer, "stopped")
            raise
        except LLMGatewayError as exc:
            if answer:
                self._save_assistant(prepared, answer, "error")
            yield {"type": "error", "code": exc.code, "message": str(exc), "partial": exc.partial}

    def _watch_rows(self, connection, user_id: int, trade_date: str) -> list[dict[str, Any]]:
        factors = self._repository.latest_factors(connection, trade_date)
        auction_scores = self._repository.latest_auction_scores(connection, trade_date)
        rows = []
        for watch in self._repository.watchlist(connection, user_id):
            factor = factors.get(str(watch["identifier"]), {})
            rows.append(
                {
                    **dict(watch),
                    "code": str(watch["identifier"]).split(".")[0],
                    "pct_chg": factor.get("pct_chg"),
                    "return_5d": factor.get("return_5d"),
                    "attention_score": auction_scores.get(str(watch["identifier"])),
                    "remark": str(watch["remark"]),
                }
            )
        return rows

    def _sync_tracking_alerts(self, user_id: int) -> None:
        tracks = self._screener.tracks(user_id)
        batches: dict[int, list[dict]] = {}
        for item in tracks:
            batches.setdefault(int(item["run_id"]), []).append(item)
        with self._database.transaction() as connection:
            for run_id, items in batches.items():
                name = str(items[0]["strategy_name"])
                if any(item.get("t1_return") is not None for item in items):
                    self._repository.save_alert(
                        connection,
                        user_id,
                        {
                            "kind": "strategy_t1",
                            "title": f"{name} 已有 T+1 反馈",
                            "content": (
                                f"{sum(item.get('t1_return') is not None for item in items)}"
                                f"/{len(items)} 只标的已有首日表现。"
                            ),
                            "available_date": date.today().isoformat(),
                            "code": "",
                            "dedupe_key": f"strategy:{run_id}:t1",
                        },
                        _now(),
                    )
                if items and all(item.get("t5_return") is not None for item in items):
                    self._repository.save_alert(
                        connection,
                        user_id,
                        {
                            "kind": "strategy_t5",
                            "title": f"{name} 五日跟踪完成",
                            "content": f"本批 {len(items)} 只标的已完成 T+5 跟踪。",
                            "available_date": date.today().isoformat(),
                            "code": "",
                            "dedupe_key": f"strategy:{run_id}:t5",
                        },
                        _now(),
                    )

    def _assistant_context(self, principal: Principal, trade_date: str) -> dict[str, Any]:
        summary = self._gateway.summary(trade_date)
        workspace = self.workspace(principal, trade_date)
        alerts = self.alert_center(principal)["items"][:20]
        return {
            "data_date": trade_date,
            "market_facts": summary["values"],
            "user_records": {
                "watchlist": workspace["watchlist"][:30],
                "daily_reviews": workspace["history"][:10],
                "strategy_tracking": self._screener.tracks(principal.user.id)[:30],
                "alerts": alerts,
                "trade_summary": workspace["trade_summary"],
                "trade_entries": workspace["trades"][:30],
            },
        }

    def _history(self, user_id: int) -> list[dict[str, str]]:
        with self._database.read() as connection:
            rows = self._repository.messages(connection, user_id, 20)
        return [
            {"role": str(row["role"]), "content": str(row["content"])[:4000]} for row in rows[-12:]
        ]

    def _save_assistant(self, prepared: PreparedAssistant, answer: str, status: str) -> None:
        if not answer:
            return
        with self._database.transaction() as connection:
            self._repository.add_message(
                connection,
                prepared.call.user_id,
                {
                    "role": "assistant",
                    "content": answer,
                    "context_date": prepared.context_date,
                    "request_id": prepared.call.request_id,
                    "status": status,
                },
                _now(),
            )

    def _trade_date(self, value: str) -> str:
        requested = _date(value)
        return self._gateway.trade_context(requested).actual_date or requested


def _date(value: str) -> str:
    try:
        return date.fromisoformat(value).isoformat()
    except ValueError as exc:
        raise ReviewError("日期格式无效。") from exc


def _text(value: Any, limit: int, required: bool = False) -> str:
    normalized = " ".join(str(value or "").split())
    if required and not normalized:
        raise ReviewError("必填内容不能为空。")
    if len(normalized) > limit:
        raise ReviewError(f"内容不能超过{limit}个字符。")
    return normalized


def _now() -> str:
    return datetime.now(SHANGHAI).isoformat(timespec="seconds")
