"""内存替身：项目/章节 repo + 写章网关（端点测试用，无 DB/无网络）。

绝对导入 `from fakes_projects import ...`（包目录无 __init__.py，见 memory/gotchas）。
全局唯一名（避免跨包同名碰撞）。
"""

from __future__ import annotations

import uuid
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from datetime import UTC, datetime
from typing import Any

from pydantic import BaseModel
from ww_core.domain import ForeshadowLedgerView, OutlineWriteView, StyleFingerprintView
from ww_core.domain.chapter_repo import ChapterDraftView, ChapterView
from ww_core.domain.foreshadow_state import (
    ForeshadowStatus,
    apply_overdue_scan,
)
from ww_core.domain.foreshadow_state import (
    transition as apply_transition,
)
from ww_core.domain.job_repo import (
    PROGRESS_COMPLETE,
    STATUS_AWAITING,
    STATUS_DONE,
    STATUS_FAILED,
    STATUS_QUEUED,
    STATUS_RUNNING,
    JobView,
)
from ww_core.domain.project_repo import ProjectCreate, ProjectView
from ww_core.domain.repositories import DigestView, OutlineView
from ww_core.domain.review_repo import ReviewView
from ww_llm_gateway.types import Delta, LlmRequest, LlmResponse, ServedBy, Usage


class FakeProjectRepo:
    """实现 `ProjectRepo` Protocol 的内存版（按 owner_id 隔离）。"""

    def __init__(self) -> None:
        self.rows: dict[uuid.UUID, tuple[uuid.UUID, ProjectView]] = {}
        self.now = datetime(2026, 6, 28, tzinfo=UTC)

    async def create(self, owner_id: uuid.UUID, data: ProjectCreate) -> ProjectView:
        pid = uuid.uuid4()
        view = ProjectView(
            id=pid,
            title=data.title,
            genre=data.genre,
            logline=data.logline,
            premise=data.premise,
            theme=data.theme,
            selling_points=list(data.selling_points),
            structure=data.structure,
            tone=data.tone,
            ending_type=data.ending_type,
            narrative_pov=data.narrative_pov,
            updated_at=self.now,
        )
        self.rows[pid] = (owner_id, view)
        return view

    async def list_for_owner(self, owner_id: uuid.UUID) -> list[ProjectView]:
        return [v for (o, v) in self.rows.values() if o == owner_id]

    async def get(self, owner_id: uuid.UUID, project_id: uuid.UUID) -> ProjectView | None:
        entry = self.rows.get(project_id)
        if entry is None or entry[0] != owner_id:
            return None
        return entry[1]


class FakeChapterRepo:
    """实现 `ChapterRepo` Protocol：幂等覆盖同 (project_id, chapter_no) 草稿。"""

    def __init__(self) -> None:
        self.drafts: dict[tuple[uuid.UUID, int], ChapterDraftView] = {}
        self.accepted_versions: dict[tuple[uuid.UUID, int], int] = {}
        self.accepted_content: dict[tuple[uuid.UUID, int, int], str] = {}

    async def save_draft(
        self, project_id: uuid.UUID, chapter_no: int, *, text: str, volume: int = 1
    ) -> ChapterDraftView:
        view = ChapterDraftView(
            project_id=project_id,
            chapter_no=chapter_no,
            volume=volume,
            content=text,
            status="draft",
            version=1,
        )
        self.drafts[(project_id, chapter_no)] = view
        return view

    async def get_draft(self, project_id: uuid.UUID, chapter_no: int) -> ChapterDraftView | None:
        return self.drafts.get((project_id, chapter_no))

    async def max_version(self, project_id: uuid.UUID, chapter_no: int) -> int:
        versions = [
            v for (p, c), v in self.accepted_versions.items() if p == project_id and c == chapter_no
        ]
        draft = 1 if (project_id, chapter_no) in self.drafts else 0
        return max([*versions, draft], default=0)

    async def promote_to_accepted(
        self, project_id: uuid.UUID, chapter_no: int, *, content: str, volume: int = 1
    ) -> ChapterView:
        next_version = (await self.max_version(project_id, chapter_no)) + 1
        self.accepted_versions[(project_id, chapter_no)] = next_version
        self.accepted_content[(project_id, chapter_no, next_version)] = content
        return ChapterView(
            project_id=project_id,
            chapter_no=chapter_no,
            volume=volume,
            content=content,
            status="accepted",
            version=next_version,
        )

    async def latest_accepted(self, project_id: uuid.UUID, chapter_no: int) -> ChapterView | None:
        ver = self.accepted_versions.get((project_id, chapter_no))
        if ver is None:
            return None
        return ChapterView(
            project_id=project_id,
            chapter_no=chapter_no,
            volume=1,
            content=self.accepted_content[(project_id, chapter_no, ver)],
            status="accepted",
            version=ver,
        )


class FakeReviewRepo:
    """实现 `ReviewRepo` Protocol（内存留痕 + 历史 + 裁决；只 flush 语义无 DB）。"""

    def __init__(self, *, fail_set_decisions: bool = False) -> None:
        self.rows: list[ReviewView] = []
        self._fail_set_decisions = fail_set_decisions

    async def record(
        self,
        project_id: uuid.UUID,
        chapter_no: int,
        *,
        chapter_version: int | None = None,
        conflicts: list[dict[str, Any]],
        foreshadow_sug: list[dict[str, Any]] | None = None,
        style: dict[str, Any] | None = None,
        pace: dict[str, Any] | None = None,
        characterization: dict[str, Any] | None = None,
        health_score: int | None = None,
    ) -> ReviewView:
        view = ReviewView(
            id=uuid.uuid4(),
            project_id=project_id,
            chapter_no=chapter_no,
            chapter_version=chapter_version,
            conflicts=[dict(c) for c in conflicts],
            foreshadow_sug=[dict(s) for s in (foreshadow_sug or [])],
            style=style,
            pace=pace,
            characterization=characterization,
            health_score=health_score,
        )
        # 新→旧：插到列表头部。
        self.rows.insert(0, view)
        return view

    async def list_for_chapter(self, project_id: uuid.UUID, chapter_no: int) -> list[ReviewView]:
        return [r for r in self.rows if r.project_id == project_id and r.chapter_no == chapter_no]

    async def set_decisions(self, review_id: uuid.UUID, *, decisions: dict[str, Any]) -> ReviewView:
        if self._fail_set_decisions:
            raise RuntimeError("boom: set_decisions failed (transaction rollback test)")
        for i, r in enumerate(self.rows):
            if r.id == review_id:
                updated = r.model_copy(update={"decisions": dict(decisions)})
                self.rows[i] = updated
                return updated
        raise LookupError(f"review not found: {review_id}")


class FakeDigestAppendRepo:
    """实现 `DigestAppendRepo` Protocol（内存 append-only）。"""

    def __init__(self) -> None:
        self.rows: list[tuple[uuid.UUID, int, dict[str, Any]]] = []

    async def append(
        self, project_id: uuid.UUID, chapter_no: int, *, facts: dict[str, Any]
    ) -> DigestView:
        self.rows.append((project_id, chapter_no, dict(facts)))
        return DigestView(chapter_no=chapter_no, facts=dict(facts))


class FakeSession:
    """最小 fake：记录 commit/rollback 次数（验收事务 + 端点提交边界断言）。"""

    def __init__(self) -> None:
        self.commits = 0
        self.rollbacks = 0

    async def commit(self) -> None:
        self.commits += 1

    async def rollback(self) -> None:
        self.rollbacks += 1


class FakeForeshadowRepo:
    """实现 `ForeshadowLedgerRepo` Protocol（内存；状态机/进展/扫描复用纯函数）。

    `raise_integrity_on` 模拟重复 code 的 DB 唯一约束冲突（register 抛 IntegrityError）。
    """

    def __init__(self, *, raise_integrity_on: set[str] | None = None) -> None:
        self.rows: dict[tuple[uuid.UUID, str], ForeshadowLedgerView] = {}
        self._raise_integrity_on = raise_integrity_on or set()

    async def register(
        self,
        project_id: uuid.UUID,
        *,
        code: str,
        title: str,
        planted_at: int | None = None,
        content: str | None = None,
        expected_close_from: int | None = None,
        expected_close_to: int | None = None,
        importance: str | None = None,
    ) -> ForeshadowLedgerView:
        if code in self._raise_integrity_on or (project_id, code) in self.rows:
            from sqlalchemy.exc import IntegrityError

            raise IntegrityError("duplicate code", None, Exception("unique"))
        view = ForeshadowLedgerView(
            code=code,
            title=title,
            status=ForeshadowStatus.OPEN.value,
            planted_at=planted_at,
            content=content,
            expected_close_from=expected_close_from,
            expected_close_to=expected_close_to,
            importance=importance,
        )
        self.rows[(project_id, code)] = view
        return view

    async def get(self, project_id: uuid.UUID, code: str) -> ForeshadowLedgerView | None:
        return self.rows.get((project_id, code))

    async def list_by_status(
        self, project_id: uuid.UUID, status: str | None = None
    ) -> list[ForeshadowLedgerView]:
        rows = [v for (p, _), v in self.rows.items() if p == project_id]
        if status is not None:
            rows = [v for v in rows if v.status == status]
        return sorted(rows, key=lambda v: v.code)

    async def transition(
        self, project_id: uuid.UUID, code: str, *, to_status: str
    ) -> ForeshadowLedgerView:
        row = self.rows.get((project_id, code))
        if row is None:
            raise LookupError(f"foreshadow not found: {code}")
        new_status = apply_transition(
            ForeshadowStatus(row.status), ForeshadowStatus(to_status)
        ).value
        updated = row.model_copy(update={"status": new_status})
        self.rows[(project_id, code)] = updated
        return updated

    async def record_progress(
        self, project_id: uuid.UUID, code: str, *, entry: dict[str, Any]
    ) -> ForeshadowLedgerView:
        row = self.rows.get((project_id, code))
        if row is None:
            raise LookupError(f"foreshadow not found: {code}")
        updated = row.model_copy(update={"progress": [*row.progress, dict(entry)]})
        self.rows[(project_id, code)] = updated
        return updated

    async def scan_overdue(
        self, project_id: uuid.UUID, *, current_chapter: int
    ) -> list[ForeshadowLedgerView]:
        changed: list[ForeshadowLedgerView] = []
        for key, row in list(self.rows.items()):
            new_status = apply_overdue_scan(
                ForeshadowStatus(row.status),
                current_chapter=current_chapter,
                expected_close_to=row.expected_close_to,
            ).value
            if new_status != row.status:
                updated = row.model_copy(update={"status": new_status})
                self.rows[key] = updated
                changed.append(updated)
        return changed


class FakeOutlineWriteRepo:
    """实现 `OutlineWriteRepo` Protocol（内存；幂等覆盖同 (project_id, chapter_no)）。"""

    def __init__(self) -> None:
        # (project_id, chapter_no) -> (volume, beats, foreshadow_windows)
        self.rows: dict[tuple[uuid.UUID, int], tuple[int, list[str], list[dict[str, Any]]]] = {}
        self.upsert_calls = 0

    async def upsert_chapter(
        self,
        project_id: uuid.UUID,
        *,
        volume: int,
        chapter_no: int,
        beats: list[str],
        foreshadow_windows: list[dict[str, Any]],
    ) -> OutlineWriteView:
        self.upsert_calls += 1
        windows = [dict(w) for w in foreshadow_windows]
        self.rows[(project_id, chapter_no)] = (volume, list(beats), windows)
        return OutlineWriteView(
            chapter_no=chapter_no,
            volume=volume,
            beats=list(beats),
            foreshadow_windows=windows,
        )


class FakeOutlineReadRepo:
    """实现读侧 `OutlineRepo` Protocol（内存；DB beats 形 `{"beats": [...]}`）。

    `add_chapter` 以裸 `list[str]` 便利入种，内部包成 dict（与 DB JSONB round-trip 一致）。
    `list_for_project` 按 chapter_no 升序返回（仿 SqlOutlineRepo 的 order_by）。
    """

    def __init__(self) -> None:
        self.rows: dict[tuple[uuid.UUID, int], OutlineView] = {}

    def add_chapter(
        self,
        project_id: uuid.UUID,
        *,
        volume: int,
        chapter_no: int,
        beats: list[str],
        foreshadow_windows: list[dict[str, Any]] | None = None,
    ) -> None:
        self.rows[(project_id, chapter_no)] = OutlineView(
            volume=volume,
            chapter_no=chapter_no,
            beats={"beats": list(beats)},
            foreshadow_windows=[dict(w) for w in (foreshadow_windows or [])],
        )

    async def get(self, project_id: uuid.UUID, chapter_no: int) -> OutlineView | None:
        return self.rows.get((project_id, chapter_no))

    async def list_for_project(self, project_id: uuid.UUID) -> list[OutlineView]:
        views = [v for (p, _), v in self.rows.items() if p == project_id]
        return sorted(views, key=lambda v: v.chapter_no)


class FakeSessionFactory:
    """到期扫描的独立 session 工厂替身：`()` → async-CM 产 `FakeSession`。

    记录开了几次 session + 各次 commit，供「扫描自建 session 并提交」断言。
    """

    def __init__(self) -> None:
        self.sessions: list[FakeSession] = []

    def __call__(self) -> Any:
        session = FakeSession()
        self.sessions.append(session)

        @asynccontextmanager
        async def _cm() -> AsyncIterator[FakeSession]:
            yield session

        return _cm()


class FakeStyleWriteRepo:
    """实现 `StyleFingerprintWriteRepo` Protocol（内存版本化 append + 最新读取）。

    `append` 模拟 version=当前 max+1（首次 1）；`latest` 取最大 version 行。
    供 `GET /style` 读侧 + 学文风 work（mode=update → 第二次 append version+1）断言。
    """

    def __init__(self) -> None:
        # version -> (dimensions, evidence)
        self.rows: dict[int, tuple[dict[str, Any], dict[str, Any]]] = {}
        self.append_calls = 0

    async def append(
        self,
        project_id: uuid.UUID,
        *,
        dimensions_json: dict[str, Any],
        evidence_json: dict[str, Any],
    ) -> int:
        self.append_calls += 1
        next_version = (max(self.rows) if self.rows else 0) + 1
        self.rows[next_version] = (dict(dimensions_json), dict(evidence_json))
        return next_version

    async def latest(self, project_id: uuid.UUID) -> StyleFingerprintView | None:
        if not self.rows:
            return None
        version = max(self.rows)
        dimensions, evidence = self.rows[version]
        return StyleFingerprintView(dimensions=dimensions, evidence=evidence, version=version)


class FakeJobRepo:
    """实现 `JobRepo` Protocol（内存）：create/get/状态流转，供 `POST /style` 断言。"""

    def __init__(self) -> None:
        self.rows: dict[uuid.UUID, JobView] = {}

    async def create(self, project_id: uuid.UUID | None, kind: str) -> JobView:
        view = JobView(
            id=uuid.uuid4(),
            project_id=project_id,
            kind=kind,
            status=STATUS_QUEUED,
            progress=0,
        )
        self.rows[view.id] = view
        return view

    async def set_running(self, job_id: uuid.UUID) -> JobView:
        view = self.rows[job_id].model_copy(update={"status": STATUS_RUNNING})
        self.rows[job_id] = view
        return view

    async def set_progress(self, job_id: uuid.UUID, pct: int) -> JobView:
        view = self.rows[job_id].model_copy(update={"progress": max(0, min(100, pct))})
        self.rows[job_id] = view
        return view

    async def complete(self, job_id: uuid.UUID, result: dict[str, Any]) -> JobView:
        view = self.rows[job_id].model_copy(
            update={"status": STATUS_DONE, "progress": PROGRESS_COMPLETE, "result": dict(result)}
        )
        self.rows[job_id] = view
        return view

    async def set_awaiting(self, job_id: uuid.UUID, result: dict[str, Any]) -> JobView:
        view = self.rows[job_id].model_copy(
            update={"status": STATUS_AWAITING, "result": dict(result)}
        )
        self.rows[job_id] = view
        return view

    async def claim_awaiting_to_running(self, job_id: uuid.UUID) -> JobView | None:
        """原子抢占 awaiting→running（内存替身：仅当现态 awaiting 才抢到，否则 None）。"""
        current = self.rows.get(job_id)
        if current is None or current.status != STATUS_AWAITING:
            return None
        view = current.model_copy(update={"status": STATUS_RUNNING})
        self.rows[job_id] = view
        return view

    async def fail(self, job_id: uuid.UUID, error: str) -> JobView:
        view = self.rows[job_id].model_copy(update={"status": STATUS_FAILED, "error": error})
        self.rows[job_id] = view
        return view

    async def get(self, job_id: uuid.UUID) -> JobView | None:
        return self.rows.get(job_id)

    async def reap_zombies(self) -> int:
        return 0


class FakeReviewGateway:
    """实现审稿/digest 节点最小依赖（`run`）：吐固定结构化 `parsed`，绝不联网。"""

    def __init__(
        self,
        parsed: BaseModel | None = None,
        error: Exception | None = None,
        *,
        text: str | None = None,
    ) -> None:
        self._parsed = parsed
        self._error = error
        # `text` 覆盖纯文本产出（refiner output_schema=None → 端点消费 resp.text）。
        self._text = text
        self.requests: list[LlmRequest] = []

    async def run(self, req: LlmRequest) -> LlmResponse:
        self.requests.append(req)
        if self._error is not None:
            raise self._error
        if self._text is not None:
            out_text = self._text
        elif self._parsed is not None:
            out_text = self._parsed.model_dump_json()
        else:
            out_text = "{}"
        return LlmResponse(
            text=out_text,
            parsed=self._parsed,
            usage=Usage(
                provider="fake",
                model="fake",
                input_tokens=1,
                output_tokens=1,
                cost_minor=0,
                currency="USD",
            ),
            served_by=ServedBy(provider="fake", model="fake"),
        )


class FakeWriterGateway:
    """实现 write 节点最小依赖（`stream`）：吐固定 `Delta` 序列，绝不联网。

    可注入 `error` 以验证 SSE 归一层把异常归一为 error 事件。
    """

    def __init__(self, chunks: list[str] | None = None, error: Exception | None = None) -> None:
        self._chunks = chunks if chunks is not None else ["第一段。", "第二段。"]
        self._error = error
        self.requests: list[LlmRequest] = []

    async def stream(self, req: LlmRequest) -> AsyncIterator[Delta]:
        self.requests.append(req)
        for c in self._chunks:
            yield Delta(text=c)
        if self._error is not None:
            raise self._error
