"""生成/入库端点（C3 扩 / ARCH §6.5 角色生成器 / §5.4 / §7.2；不变量 #1/#3/#9）。

三类端点 + 两个读端点（M5-d 生成走即时返回：预览 → 作者确认 → 入库，非 jobs）：
- `POST /projects/:id/world/generate`：worldbuilder（writer 网关）→ **预览**世界观实体（不入库）。
- `POST /projects/:id/characters/generate`：character-gen（writer 网关，群像防雷同）→ 预览角色卡。
- `POST /projects/:id/characters`：作者确认的角色卡入库——**入库前 gate**：
  1. `precheck_generated_cards`（continuity 预检，analyst 网关）比对生成卡 vs 真相源；
     有冲突且未 `acknowledge_conflicts` → **409 CONFLICT_UNRESOLVED**（不静默入库，仿 accept gate，
     守不变量 #3）；作者确认后放行。
  2. `partition_writes`：按 character-gen 声明的 `writes` 白名单过滤（越权表丢弃 + 审计），守 §5.6。
  3. 写 `characters` 行（schema list/str → DB JSONB dict 形变，见 character_repo）。
- `GET /projects/:id/rules`：规则列表（规则页，T5.6）。
- `GET /skills`：技能库列表（技能库 UI，T5.6）。

提交边界：网关 ledger + 角色写侧均只 flush → 端点末尾一次 `commit()`（生成预览也 commit
ledger，否则 usage 静默丢失，同 draft/review 纪律）。无凭据 → `LLM_UNAVAILABLE`（503）。
"""

from __future__ import annotations

import uuid
from typing import Annotated, Any

from fastapi import APIRouter, Depends, Request
from sqlalchemy.ext.asyncio import AsyncSession
from ww_agents import (
    CharacterCard,
    CharacterRelation,
    ProjectPlanResult,
    character_gen_spec,
    continuity_spec,
    project_plan_spec,
    worldbuilder_spec,
)
from ww_core.domain import (
    CharacterWriteRepo,
    ProjectRepo,
    RuleWriteRepo,
)
from ww_core.domain.repositories import MemoryRepos
from ww_core.orchestrator import (
    build_brief_context,
    precheck_generated_cards,
    run_character_gen,
    run_generator,
    run_worldbuilder,
)
from ww_db import get_session
from ww_llm_gateway import Gateway
from ww_shared import AppError, ErrorCode
from ww_skills import SkillRegistry, partition_writes

from ww_api.logging_config import get_logger
from ww_api.schemas.generation import (
    CharacterCardView,
    CharacterGenerateRequest,
    CharacterGenPreviewResponse,
    CharacterIngestRequest,
    CharacterIngestResponse,
    CharacterListResponse,
    CharacterRelationView,
    IngestConflictView,
    RuleListResponse,
    SkillListResponse,
    SkillView,
    WorldEntityCardView,
    WorldEntityListResponse,
    WorldGenerateRequest,
    WorldGenPreviewResponse,
)
from ww_api.schemas.projects import ProjectPlanGenerateRequest, ProjectPlanView
from ww_api.schemas.rules import RuleView
from ww_api.services.credentials import STUB_OWNER_ID
from ww_api.services.project_deps import (
    get_character_gen_gateway,
    get_character_write_repo,
    get_memory_repos,
    get_precheck_gateway,
    get_project_plan_gateway,
    get_project_repo,
    get_rule_write_repo,
    get_skill_registry,
    get_worldbuilder_gateway,
)

log = get_logger("ww.api.generation")

router = APIRouter(prefix="/projects", tags=["generation"])
skills_router = APIRouter(prefix="/skills", tags=["skills"])

ProjectRepoDep = Annotated[ProjectRepo, Depends(get_project_repo)]
MemoryReposDep = Annotated[MemoryRepos, Depends(get_memory_repos)]
CharacterWriteRepoDep = Annotated[CharacterWriteRepo, Depends(get_character_write_repo)]
RuleRepoDep = Annotated[RuleWriteRepo, Depends(get_rule_write_repo)]
SkillRegistryDep = Annotated[SkillRegistry, Depends(get_skill_registry)]
WorldGatewayDep = Annotated[Gateway, Depends(get_worldbuilder_gateway)]
CharacterGatewayDep = Annotated[Gateway, Depends(get_character_gen_gateway)]
PrecheckGatewayDep = Annotated[Gateway, Depends(get_precheck_gateway)]
ProjectPlanGatewayDep = Annotated[Gateway, Depends(get_project_plan_gateway)]
SessionDep = Annotated[AsyncSession, Depends(get_session)]


def _project_context(title: str, genre: str | None, premise: str | None, theme: str | None) -> str:
    """确定性序列化作品设定（喂 worldbuilder；无时间戳/UUID）。"""
    lines = [f"标题：{title}"]
    if genre:
        lines.append(f"题材：{genre}")
    if premise:
        lines.append(f"前提：{premise}")
    if theme:
        lines.append(f"主题：{theme}")
    return "\n".join(lines)


def _plan_seed_context(body: ProjectPlanGenerateRequest) -> str:
    """把立项向导草稿序列化为作品种子（喂 project-plan；仿 `_project_context`）。

    确定性、无时间戳/UUID；只拼非空字段（有则贴合、无则不臆造）。
    genre + logline 由端点种子门控保证非空。
    """
    lines: list[str] = []
    label_fields: list[tuple[str, str | None]] = [
        ("暂定书名", body.title),
        ("题材", body.genre),
        ("一句话故事", body.logline),
        ("立意", body.premise),
        ("主题", body.theme),
        ("故事结构", body.structure),
        ("基调", body.tone),
        ("结局取向", body.ending_type),
        ("叙事视角", body.narrative_pov),
    ]
    for label, value in label_fields:
        text = (value or "").strip()
        if text:
            lines.append(f"{label}：{text}")
    points = [p.strip() for p in body.selling_points if p.strip()]
    if points:
        lines.append(f"核心卖点：{'、'.join(points)}")
    return "\n".join(lines)


def _plan_to_view(plan: ProjectPlanResult) -> ProjectPlanView:
    """ww_agents.ProjectPlanResult → API ProjectPlanView（字段同名直传）。"""
    return ProjectPlanView(
        title_candidates=list(plan.title_candidates),
        setting=plan.setting,
        narrative_structure=plan.narrative_structure,
        story_core=plan.story_core,
        ending_design=plan.ending_design,
        tone=plan.tone,
    )


def _render_characters_context(cards: list[CharacterCard]) -> str:
    """已有角色简表（喂 character-gen 防雷同 + precheck 真相源）。确定性、保序。"""
    if not cards:
        return ""
    return "\n".join(f"- {c.name}（{c.role}）：{'、'.join(c.traits) or '（未列）'}" for c in cards)


def _relations_from_jsonb(raw: list[Any]) -> list[CharacterRelation]:
    """`characters.relations` JSONB list（每条 {name, kind, note?}）→ schema CharacterRelation。

    跳过缺 name/kind 的脏条目（历史/外部数据不可信，守输入校验边界）。
    """
    out: list[CharacterRelation] = []
    for item in raw or []:
        if not isinstance(item, dict):
            continue
        name = item.get("name")
        kind = item.get("kind")
        if not name or not kind:
            continue
        out.append(CharacterRelation(name=str(name), kind=str(kind), note=item.get("note")))
    return out


async def _existing_characters(memory: MemoryRepos, project_id: uuid.UUID) -> list[CharacterCard]:
    """把已有角色读侧视图转成 `CharacterCard`（喂防雷同 / precheck / 设定库读端点）。

    DB JSONB dict 列 → schema list/str 反向解包（与写侧形变互逆；缺则空/占位）。
    `relations` 从 JSONB list 还原（设定库 Codex 需展示关系网；precheck 不读此字段，无害）。
    """
    views = await memory.character.list_for_project(project_id)
    cards: list[CharacterCard] = []
    for v in views:
        traits = list((v.traits or {}).get("items", [])) if isinstance(v.traits, dict) else []
        tics = (
            list((v.speech_tics or {}).get("items", [])) if isinstance(v.speech_tics, dict) else []
        )
        arc = (v.arc or {}).get("text", "") if isinstance(v.arc, dict) else ""
        cards.append(
            CharacterCard(
                name=v.name,
                role=v.role or "",
                traits=traits,
                appearance=v.appearance or "",
                motive=v.motive or "",
                backstory=v.backstory or "",
                arc=arc or "",
                speech_tics=tics,
                tags=list(v.tags or []),
                relations=_relations_from_jsonb(list(v.relations or [])),
            )
        )
    return cards


async def _world_context(memory: MemoryRepos, project_id: uuid.UUID) -> str:
    """已有世界观硬规则简表（喂 character-gen / precheck 真相源）。"""
    views = await memory.world_entity.list_for_project(project_id)
    lines: list[str] = []
    for v in views:
        rules = list((v.rules or {}).get("rules", [])) if isinstance(v.rules, dict) else []
        rule_text = "；".join(rules) if rules else "（无硬规则）"
        lines.append(f"- [{v.type}] {v.name}：{rule_text}")
    return "\n".join(lines)


# ---- 世界观生成（预览，不入库）----


@router.post("/{project_id}/world/generate")
async def generate_world(
    project_id: uuid.UUID,
    body: WorldGenerateRequest,
    request: Request,
    project_repo: ProjectRepoDep,
    gateway: WorldGatewayDep,
    session: SessionDep,
) -> WorldGenPreviewResponse:
    """生成世界观实体预览（不持久化；作者确认后另入库）。无凭据 → 503；项目不存在 → 404。"""
    request_id = getattr(request.state, "request_id", None)
    project = await project_repo.get(STUB_OWNER_ID, project_id)
    if project is None:
        raise AppError(ErrorCode.NOT_FOUND, f"project not found: {project_id}")

    project_context = _project_context(project.title, project.genre, project.premise, project.theme)
    result = await run_worldbuilder(
        worldbuilder_spec,
        brief=body.brief,
        project_context=project_context,
        gateway=gateway,
        user_id=STUB_OWNER_ID,
        project_id=project_id,
    )
    # 提交边界：预览不写业务表，但网关 ledger 只 flush → 末尾 commit 落 usage（否则丢失）。
    await session.commit()
    log.info(
        "world_generate_done",
        project_id=str(project_id),
        request_id=request_id,
        entity_count=len(result.entities),
    )
    return WorldGenPreviewResponse(
        entities=[
            WorldEntityCardView(type=e.type, name=e.name, rules=list(e.rules))
            for e in result.entities
        ]
    )


# ---- 角色生成（预览，不入库；群像防雷同）----


@router.post("/{project_id}/characters/generate")
async def generate_characters(
    project_id: uuid.UUID,
    body: CharacterGenerateRequest,
    request: Request,
    project_repo: ProjectRepoDep,
    memory: MemoryReposDep,
    gateway: CharacterGatewayDep,
    session: SessionDep,
) -> CharacterGenPreviewResponse:
    """生成角色卡预览（不持久化；注入已有角色防雷同）。无凭据 → 503；项目不存在 → 404。"""
    request_id = getattr(request.state, "request_id", None)
    project = await project_repo.get(STUB_OWNER_ID, project_id)
    if project is None:
        raise AppError(ErrorCode.NOT_FOUND, f"project not found: {project_id}")

    existing = await _existing_characters(memory, project_id)
    world_context = await _world_context(memory, project_id)
    result = await run_character_gen(
        character_gen_spec,
        brief=body.brief,
        count=body.count,
        role=body.role,
        world_context=world_context,
        existing_chars=existing,
        generated_so_far=[],
        gateway=gateway,
        user_id=STUB_OWNER_ID,
        project_id=project_id,
        role_mix=body.role_mix,
    )
    await session.commit()
    log.info(
        "characters_generate_done",
        project_id=str(project_id),
        request_id=request_id,
        card_count=len(result.cards),
    )
    return CharacterGenPreviewResponse(cards=[_card_to_view(c) for c in result.cards])


# ---- 角色入库（作者确认后；continuity gate + 权限白名单）----


@router.post("/{project_id}/characters", status_code=201)
async def ingest_characters(
    project_id: uuid.UUID,
    body: CharacterIngestRequest,
    request: Request,
    project_repo: ProjectRepoDep,
    memory: MemoryReposDep,
    char_repo: CharacterWriteRepoDep,
    gateway: PrecheckGatewayDep,
    session: SessionDep,
) -> CharacterIngestResponse:
    """入库作者确认的角色卡。有 continuity 冲突且未确认 → 409；越权写表丢弃 + 审计。"""
    request_id = getattr(request.state, "request_id", None)
    project = await project_repo.get(STUB_OWNER_ID, project_id)
    if project is None:
        raise AppError(ErrorCode.NOT_FOUND, f"project not found: {project_id}")

    cards = [_view_to_card(v) for v in body.cards]

    # gate 1：入库前 continuity 预检（编排器追加的检查，非 character-gen 互调，守不变量 #1）。
    world_context = await _world_context(memory, project_id)
    existing = await _existing_characters(memory, project_id)
    conflicts = await precheck_generated_cards(
        continuity_spec,
        cards=cards,
        world_context=world_context,
        characters_context=_render_characters_context(existing),
        gateway=gateway,
        user_id=STUB_OWNER_ID,
        project_id=project_id,
    )
    if conflicts and not body.acknowledge_conflicts:
        # 仿 accept 的冲突 gate：不静默入库，须作者裁决/确认（不变量 #3）。
        await session.commit()  # 落 precheck 网关 usage（否则丢失）；不写业务表。
        log.info(
            "characters_ingest_blocked_by_conflicts",
            project_id=str(project_id),
            request_id=request_id,
            conflict_count=len(conflicts),
        )
        raise AppError(
            ErrorCode.CONFLICT_UNRESOLVED,
            "生成角色卡与现有设定存在 continuity 冲突，请裁决后确认入库",
            {
                "conflicts": [
                    IngestConflictView(
                        type=c.type, where=c.where, refs=list(c.refs), suggestion=c.suggestion
                    ).model_dump()
                    for c in conflicts
                ],
                "conflict_count": len(conflicts),
            },
        )

    # gate 2：权限白名单——character-gen 只声明 writes=["characters"]，越权产出丢弃 + 审计。
    allowed, rejected = partition_writes(character_gen_spec, {"characters": cards})
    if rejected:
        log.warning(
            "characters_ingest_rejected_over_permission",
            project_id=str(project_id),
            request_id=request_id,
            rejected_tables=rejected,
        )

    created: list[str] = []
    for card in allowed.get("characters", []):
        view = await char_repo.create(
            project_id,
            name=card.name,
            role=card.role,
            traits=list(card.traits),
            appearance=card.appearance,
            motive=card.motive,
            backstory=card.backstory,
            arc=card.arc,
            speech_tics=list(card.speech_tics),
            tags=list(card.tags),
            relations=[r.model_dump() for r in card.relations],
        )
        created.append(view.name)

    # 提交边界：网关 ledger（precheck）+ 角色写侧均只 flush → 端点末尾一次 commit。
    await session.commit()
    log.info(
        "characters_ingest_done",
        project_id=str(project_id),
        request_id=request_id,
        created_count=len(created),
    )
    return CharacterIngestResponse(created=created, rejected_tables=rejected)


# ---- 读端点：规则列表 + 技能库（T5.6 前端）----


@router.get("/{project_id}/characters")
async def list_characters(
    project_id: uuid.UUID,
    memory: MemoryReposDep,
    project_repo: ProjectRepoDep,
) -> CharacterListResponse:
    """已入库角色全量列表（设定库 Codex 真源；复用 C5 读侧 SqlCharacterRepo）。

    项目不存在 → 404（与写端点一致，避免不存在 project 返回误导性空 200）。
    DB JSONB dict 列 → API list/str 反向解包（入库形变的逆向，见 `_existing_characters`）。
    """
    if await project_repo.get(STUB_OWNER_ID, project_id) is None:
        raise AppError(ErrorCode.NOT_FOUND, f"project {project_id} not found")
    cards = await _existing_characters(memory, project_id)
    return CharacterListResponse(characters=[_card_to_view(c) for c in cards])


@router.get("/{project_id}/world_entities")
async def list_world_entities(
    project_id: uuid.UUID,
    memory: MemoryReposDep,
    project_repo: ProjectRepoDep,
) -> WorldEntityListResponse:
    """已入库世界观实体全量列表（设定库 Codex 真源；复用 C5 读侧 SqlWorldEntityRepo）。

    项目不存在 → 404（与写端点一致）。
    DB `rules` JSONB dict `{"rules":[...]}` → 裸 list（worldbuilder 形变的逆向）。
    """
    if await project_repo.get(STUB_OWNER_ID, project_id) is None:
        raise AppError(ErrorCode.NOT_FOUND, f"project {project_id} not found")
    views = await memory.world_entity.list_for_project(project_id)
    entities = [
        WorldEntityCardView(
            type=v.type,
            name=v.name,
            rules=(list((v.rules or {}).get("rules", [])) if isinstance(v.rules, dict) else []),
        )
        for v in views
    ]
    return WorldEntityListResponse(world_entities=entities)


@router.get("/{project_id}/rules")
async def list_rules(
    project_id: uuid.UUID,
    repo: RuleRepoDep,
    project_repo: ProjectRepoDep,
) -> RuleListResponse:
    """规则列表（带 id，供前端删除 handle）。项目不存在 → 404（QA MEDIUM：此前返误导性空 200）。"""
    if await project_repo.get(STUB_OWNER_ID, project_id) is None:
        raise AppError(ErrorCode.NOT_FOUND, f"project not found: {project_id}")
    rules = await repo.list_for_project(project_id)
    return RuleListResponse(
        rules=[RuleView(id=r.id, level=r.level, content=r.content) for r in rules]
    )


@skills_router.get("")
async def list_skills(registry: SkillRegistryDep) -> SkillListResponse:
    """技能库列表（按 name 升序）。技能库 UI 用。"""
    skills = [
        SkillView(
            name=spec.name,
            scope=spec.scope,
            tier=spec.tier,
            reads=list(spec.reads),
            writes=list(spec.writes),
            genre=spec.genre,
        )
        # registry.names() 已排序；按名取 spec 保证确定性顺序。
        for spec in (registry.get(name) for name in registry.names())
    ]
    return SkillListResponse(skills=skills)


# ---- AI 立项方案生成（灵感⑤；不带 project 前缀，立项前 project 未建）----


@skills_router.post("/project-plan/generate")
async def generate_project_plan(
    body: ProjectPlanGenerateRequest,
    request: Request,
    gateway: ProjectPlanGatewayDep,
    session: SessionDep,
) -> ProjectPlanView:
    """生成结构化立项方案预览（向导阶段，project 未建，不入库）。

    **不带 project 前缀**——绕开 toolbox 的 `project_repo.get→404`（立项前无 project）。
    **种子门控**：缺 genre 或 logline → 422（空向导不出方案，防 slop）。无凭据 → 503。
    """
    request_id = getattr(request.state, "request_id", None)
    genre = (body.genre or "").strip()
    logline = (body.logline or "").strip()
    if not genre or not logline:
        raise AppError(
            ErrorCode.VALIDATION,
            "生成立项方案至少需要「题材」与「一句话故事（logline）」两个种子",
            {"has_genre": bool(genre), "has_logline": bool(logline)},
        )

    project_context = _plan_seed_context(body)
    context = build_brief_context(brief=body.brief or "", project_context=project_context)
    parsed = await run_generator(
        project_plan_spec,
        context=context,
        gateway=gateway,
        user_id=STUB_OWNER_ID,
        project_id=None,  # 向导阶段 project 未建；usage_ledger.project_id 可空
    )
    # 提交边界：预览不写业务表，但网关 ledger 只 flush → 末尾 commit 落 usage（否则丢失）。
    await session.commit()
    if not isinstance(parsed, ProjectPlanResult):
        # run_generator 已按 output_schema 校验；防御性收窄（不应触达）。
        raise AppError(ErrorCode.INTERNAL, "立项方案生成返回类型异常")
    log.info(
        "project_plan_generate_done",
        request_id=request_id,
        title_count=len(parsed.title_candidates),
    )
    return _plan_to_view(parsed)


# ---- schema <-> ww_agents 卡转换 ----


def _card_to_view(card: CharacterCard) -> CharacterCardView:
    return CharacterCardView(
        name=card.name,
        role=card.role,
        traits=list(card.traits),
        appearance=card.appearance,
        motive=card.motive,
        backstory=card.backstory,
        arc=card.arc,
        speech_tics=list(card.speech_tics),
        tags=list(card.tags),
        relations=[
            CharacterRelationView(name=r.name, kind=r.kind, note=r.note) for r in card.relations
        ],
    )


def _view_to_card(view: CharacterCardView) -> CharacterCard:
    return CharacterCard(
        name=view.name,
        role=view.role,
        traits=list(view.traits),
        appearance=view.appearance,
        motive=view.motive,
        backstory=view.backstory,
        arc=view.arc,
        speech_tics=list(view.speech_tics),
        tags=list(view.tags),
        relations=[
            CharacterRelation(name=r.name, kind=r.kind, note=r.note) for r in view.relations
        ],
    )
