from __future__ import annotations

import json
import secrets
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import date, datetime
from pathlib import Path
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.accounts.service import AccountService
from backend.features.heaven import fortune, trend
from backend.features.heaven.hexagram import from_lines
from backend.features.heaven.prompt import PROMPT_VERSION, messages
from backend.features.heaven.repository import HeavenRepository
from backend.llm.gateway import LLMCall, LLMGateway, LLMGatewayError

SHANGHAI = ZoneInfo("Asia/Shanghai")


class HeavenError(RuntimeError):
    pass


@dataclass(frozen=True, slots=True)
class PreparedInterpretation:
    reading_id: int
    user_id: int
    mode: str
    prompt: list[dict[str, str]]
    call: LLMCall | None
    cached: str = ""


class HeavenService:
    def __init__(
        self,
        database: Database,
        repository: HeavenRepository,
        gateway: DataGateway,
        accounts: AccountService,
        llm: LLMGateway,
        iching_path: Path,
    ) -> None:
        self._database = database
        self._repository = repository
        self._gateway = gateway
        self._accounts = accounts
        self._llm = llm
        self._iching_path = iching_path

    def setup(self, principal: Principal, requested_date: str) -> dict[str, Any]:
        reading_date = self._date(requested_date)
        field = fortune.build(reading_date, self._accounts.get_profile(principal.user.id))
        with self._database.read() as connection:
            daily = self._repository.latest_fortune(connection, principal.user.id, reading_date)
            rows = self._repository.list(connection, principal.user.id, None, None, 60)
        return {
            "date": reading_date,
            "fortune": field,
            "daily_fortune": _public(daily) if daily else None,
            "history": [_public(row) for row in rows],
        }

    def trend(
        self,
        principal: Principal,
        query: str,
        requested_date: str,
        manual: dict[str, Any],
    ) -> dict[str, Any]:
        reading_date = self._date(requested_date)
        payload = self._gateway.heaven_trend_inputs(query, reading_date)
        try:
            result = trend.calculate(payload, self._iching_path, manual)
        except trend.TrendDataError as exc:
            return {
                "ready": False,
                "message": str(exc),
                "trade_date": payload.get("trade_date"),
                "stock": payload.get("stock"),
                "sector": payload.get("sector"),
                "checks": exc.checks,
                "automatic": exc.payload,
            }
        reading_id = self._save(
            principal.user.id,
            "trend",
            result["trade_date"],
            str(result["stock"]["identifier"]),
            result,
        )
        return {"ready": True, "reading_id": reading_id, "result": result}

    def fortune(self, principal: Principal, requested_date: str) -> dict[str, Any]:
        reading_date = self._date(requested_date)
        result = fortune.build(reading_date, self._accounts.get_profile(principal.user.id))
        with self._database.transaction() as connection:
            row, reused = self._repository.ensure_fortune(
                connection,
                user_id=principal.user.id,
                reading_date=reading_date,
                result=result,
                created_at=_now(),
            )
        return {"reused": reused, "reading": _public(row)}

    def heart_line(
        self, principal: Principal, requested_date: str, values: list[int]
    ) -> dict[str, Any]:
        self._date(requested_date)
        if len(values) >= 6 or any(value not in {6, 7, 8, 9} for value in values):
            raise HeavenError("当前起卦进度无效。")
        faces = [2 + secrets.randbelow(2) for _ in range(3)]
        value = sum(faces)
        return {
            "position": len(values) + 1,
            "value": value,
            "faces": ["front" if face == 3 else "back" for face in faces],
            "values": [*values, value],
        }

    def complete_heart(
        self,
        principal: Principal,
        requested_date: str,
        values: list[int],
        first_thought_confirmed: bool,
    ) -> dict[str, Any]:
        reading_date = self._date(requested_date)
        if not first_thought_confirmed:
            raise HeavenError("请先确认第一念，再进入解卦。")
        result = {
            "date": reading_date,
            "hexagram": from_lines(values, self._iching_path),
            "notice": "卦象仅供传统文化与自我观察，不构成预测或投资建议。",
        }
        reading_id = self._save(principal.user.id, "heart", reading_date, "", result)
        return {"reading_id": reading_id, "result": result}

    def readings(
        self, principal: Principal, mode: str | None, requested_date: str | None
    ) -> list[dict[str, Any]]:
        reading_date = self._date(requested_date) if requested_date else None
        if mode and mode not in {"trend", "fortune", "heart"}:
            raise HeavenError("历史类型无效。")
        with self._database.read() as connection:
            rows = self._repository.list(connection, principal.user.id, mode, reading_date, 60)
        return [_public(row) for row in rows]

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

    def prepare_interpret(self, principal: Principal, reading_id: int) -> PreparedInterpretation:
        with self._database.read() as connection:
            row = self._repository.get(connection, principal.user.id, reading_id)
        if row is None:
            raise HeavenError("未找到该问天记录。")
        if row["interpretation_status"] == "complete" and row["interpretation"]:
            return PreparedInterpretation(
                reading_id,
                principal.user.id,
                str(row["mode"]),
                [],
                None,
                str(row["interpretation"]),
            )
        result = json.loads(str(row["result_json"]))
        prompt = messages(str(row["mode"]), result)
        call = self._llm.prepare(
            principal,
            feature=f"heaven_{row['mode']}",
            prompt_version=PROMPT_VERSION,
            business_id=f"heaven:{reading_id}",
            input_chars=sum(len(item["content"]) for item in prompt),
        )
        return PreparedInterpretation(reading_id, principal.user.id, str(row["mode"]), prompt, call)

    def stream_interpret(self, prepared: PreparedInterpretation) -> Iterator[dict[str, Any]]:
        if prepared.cached:
            yield {"type": "delta", "content": prepared.cached, "cached": True}
            yield {"type": "done", "cached": True}
            return
        if prepared.call is None:
            raise HeavenError("智能解读状态无效。")
        answer = ""
        saved = False
        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_interpretation(prepared, answer, "complete")
                    saved = True
                    yield {"type": "done", "request_id": event.request_id}
        except GeneratorExit:
            stream.close()
            if answer and not saved:
                self._save_interpretation(prepared, answer, "stopped")
            raise
        except LLMGatewayError as exc:
            if answer:
                self._save_interpretation(prepared, answer, "error")
            yield {"type": "error", "code": exc.code, "message": str(exc), "partial": exc.partial}

    def _save(
        self, user_id: int, mode: str, reading_date: str, subject_key: str, result: dict[str, Any]
    ) -> int:
        now = _now()
        with self._database.transaction() as connection:
            return self._repository.add(
                connection,
                user_id=user_id,
                mode=mode,
                reading_date=reading_date,
                subject_key=subject_key,
                result=result,
                created_at=now,
            )

    def _save_interpretation(
        self, prepared: PreparedInterpretation, answer: str, status: str
    ) -> None:
        with self._database.transaction() as connection:
            self._repository.update_interpretation(
                connection,
                prepared.reading_id,
                prepared.user_id,
                answer,
                status,
                prepared.call.request_id if prepared.call else None,
                _now(),
            )

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


def _public(row: Any) -> dict[str, Any]:
    return {
        "id": int(row["id"]),
        "mode": str(row["mode"]),
        "date": str(row["reading_date"]),
        "subject_key": str(row["subject_key"]),
        "result": json.loads(str(row["result_json"])),
        "interpretation": str(row["interpretation"]),
        "status": str(row["interpretation_status"]),
        "created_at": str(row["created_at"]),
    }


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