from __future__ import annotations

import json
import re
from datetime import UTC, datetime, timedelta
from typing import Any

from fastapi import HTTPException, status
from sqlalchemy import select
from sqlalchemy.orm import Session

from app.core.config import get_settings
from app.models.chat import ChatMessage, ChatSession, TopicSession
from app.models.growth import GrowthProfileRevision, TopicSummary, UserGrowthProfile
from app.models.user import User
from app.services.entitlement_service import EntitlementService
from app.services.external_errors import ExternalServiceError
from app.services.tracked_generation_service import TrackedGenerationService


RECENT_REVIEW_SCHEMA_VERSION = 2
RECENT_REVIEW_DAYS = 30
RECENT_REVIEW_TOPIC_LIMIT = 10


class GrowthProfileService:
    @staticmethod
    def finish_active_topic(db: Session, *, user: User, session: ChatSession, force: bool = False) -> dict:
        topic = db.scalar(
            select(TopicSession)
            .where(
                TopicSession.user_id == user.id,
                TopicSession.chat_session_id == session.id,
                TopicSession.status == "active",
            )
            .order_by(TopicSession.created_at.desc(), TopicSession.id.desc())
            .with_for_update()
            .limit(1)
        )
        if topic is None:
            # 重复点击或客户端超时重试时返回最近一次任务，不重复生成摘要。
            topic = db.scalar(
                select(TopicSession)
                .where(
                    TopicSession.user_id == user.id,
                    TopicSession.chat_session_id == session.id,
                    TopicSession.status == "completed",
                )
                .order_by(TopicSession.ended_at.desc(), TopicSession.id.desc())
                .limit(1)
            )
            summary = (
                db.scalar(select(TopicSummary).where(TopicSummary.topic_session_id == topic.id))
                if topic is not None
                else None
            )
            if topic is None or summary is None:
                raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="当前会话没有进行中的主题")
            entitlement = EntitlementService.active_entitlement(db, user)
            return _settlement_result(db, topic=topic, summary=summary, user=user, growth_enabled=entitlement.enable_growth_profile)

        has_messages = db.scalar(
            select(ChatMessage.id)
            .where(ChatMessage.topic_session_id == topic.id, ChatMessage.user_id == user.id)
            .limit(1)
        )
        if has_messages is None:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="当前主题还没有可沉淀的对话内容")

        summary = db.scalar(select(TopicSummary).where(TopicSummary.topic_session_id == topic.id))
        is_new_summary = summary is None
        if is_new_summary:
            summary = TopicSummary(topic_session_id=topic.id, user_id=user.id, summary="")
        if is_new_summary or force or summary.status not in {"pending", "running", "success", "fallback"}:
            _reset_summary_job(summary)
        summary.max_attempts = max(1, get_settings().topic_settlement_max_attempts)
        db.add(summary)

        topic.status = "completed"
        topic.ended_at = _now()
        db.add(topic)

        entitlement = EntitlementService.active_entitlement(db, user)
        db.commit()
        db.refresh(topic)
        db.refresh(summary)
        return _settlement_result(db, topic=topic, summary=summary, user=user, growth_enabled=entitlement.enable_growth_profile)

    @staticmethod
    def retry_topic_settlement(db: Session, *, user: User, topic: TopicSession) -> TopicSummary:
        summary = db.scalar(select(TopicSummary).where(TopicSummary.topic_session_id == topic.id).with_for_update())
        if summary is None:
            summary = TopicSummary(topic_session_id=topic.id, user_id=user.id, summary="")
        if summary.status == "running":
            return summary
        _reset_summary_job(summary)
        summary.max_attempts = max(1, get_settings().topic_settlement_max_attempts)
        db.add(summary)
        db.flush()
        return summary

    @staticmethod
    def topic_settlement_result(db: Session, *, user: User, summary_id: int) -> dict:
        summary = db.scalar(
            select(TopicSummary).where(TopicSummary.id == summary_id, TopicSummary.user_id == user.id)
        )
        if summary is None:
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="主题沉淀任务不存在")
        topic = db.get(TopicSession, summary.topic_session_id)
        if topic is None:
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="主题不存在")
        entitlement = EntitlementService.active_entitlement(db, user)
        return _settlement_result(
            db,
            topic=topic,
            summary=summary,
            user=user,
            growth_enabled=entitlement.enable_growth_profile,
        )

    @staticmethod
    def process_topic_settlement(db: Session, *, summary: TopicSummary) -> tuple[TopicSummary, UserGrowthProfile | None]:
        topic = db.get(TopicSession, summary.topic_session_id)
        # Redis 失效时允许多个进程依靠数据库继续消费；用户行锁保证同一学员的
        # 多个主题不会并发覆盖近期实修回顾。
        user = db.scalar(select(User).where(User.id == summary.user_id).with_for_update())
        if topic is None or user is None or user.is_deleted:
            raise ValueError("主题或用户不存在")
        generated = GrowthProfileService.generate_topic_summary(
            db,
            user=user,
            topic=topic,
            force=True,
            process_job=True,
            allow_fallback=False,
        )
        # V2 不再固化或展示“建议功课”，历史列仅保留兼容。
        topic.recommended_homework = None
        db.add(topic)
        entitlement = EntitlementService.active_entitlement(db, user)
        profile = None
        if entitlement.enable_growth_profile:
            profile = GrowthProfileService.update_growth_profile(db, user=user, topic_summary=generated)
        return generated, profile

    @staticmethod
    def generate_topic_summary(
        db: Session,
        *,
        user: User,
        topic: TopicSession,
        force: bool = False,
        process_job: bool = False,
        allow_fallback: bool = True,
    ) -> TopicSummary:
        existing = db.scalar(select(TopicSummary).where(TopicSummary.topic_session_id == topic.id))
        if existing is not None and existing.schema_version < RECENT_REVIEW_SCHEMA_VERSION:
            force = True
        if existing is not None and not force:
            if existing.status in {"success", "fallback"}:
                return existing
            if existing.status in {"pending", "running"}:
                raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="主题正在后台沉淀，请稍后再试")
            raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="主题沉淀失败，请稍后重试")
        if existing is not None and existing.status in {"pending", "running"} and not process_job:
            raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="主题正在后台沉淀，请稍后再试")
        messages = list(
            db.scalars(
                select(ChatMessage)
                .where(ChatMessage.topic_session_id == topic.id, ChatMessage.user_id == user.id)
                .order_by(ChatMessage.created_at.asc(), ChatMessage.id.asc())
            )
        )
        if not messages:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="当前主题还没有可沉淀的对话内容")
        user_content = _messages_text(messages, roles={"user"})
        assistant_context = _messages_text(messages, roles={"assistant"})
        prompt = _topic_summary_prompt(topic, user_content, assistant_context)
        model_name = None
        try:
            completion = TrackedGenerationService.generate(
                db,
                prompt=prompt,
                scenario="summary",
                user_id=user.id,
            )
            raw = completion.answer
            model_name = completion.model_name
            parsed = _parse_summary_json(raw)
            data = parsed if parsed and "summary" in parsed else _fallback_summary(topic, user_content, raw)
            status_value = "success"
            error_message = None
        except ExternalServiceError as exc:
            if not allow_fallback:
                raise
            data = _fallback_summary(topic, user_content, "")
            status_value = "fallback"
            error_message = str(exc)

        if existing is None:
            existing = TopicSummary(topic_session_id=topic.id, user_id=user.id)
        _apply_summary(existing, data)
        existing.schema_version = RECENT_REVIEW_SCHEMA_VERSION
        existing.model_name = model_name
        existing.status = status_value
        existing.error_message = error_message
        existing.generated_at = _now()
        existing.finished_at = _now()
        existing.next_run_at = None
        existing.locked_at = None
        existing.locked_by = None
        db.add(existing)
        db.flush()
        return existing

    @staticmethod
    def update_growth_profile(db: Session, *, user: User, topic_summary: TopicSummary) -> UserGrowthProfile:
        profile = db.scalar(select(UserGrowthProfile).where(UserGrowthProfile.user_id == user.id))
        if (
            profile is not None
            and profile.schema_version >= RECENT_REVIEW_SCHEMA_VERSION
            and profile.last_topic_summary_id == topic_summary.id
        ):
            return profile
        before = growth_profile_dict(profile) if profile is not None else None
        if profile is None:
            profile = UserGrowthProfile(user_id=user.id, profile_text="")
            db.add(profile)
            db.flush()

        summaries = _recent_review_summaries(db, user_id=user.id)
        prompt = _recent_review_prompt(summaries)
        try:
            completion = TrackedGenerationService.generate(
                db,
                prompt=prompt,
                scenario="summary",
                user_id=user.id,
            )
            raw = completion.answer
            parsed = _parse_summary_json(raw)
            data = parsed if parsed and "reviewText" in parsed else _fallback_recent_review(summaries, raw)
        except ExternalServiceError:
            data = _fallback_recent_review(summaries, "")

        _apply_recent_review(profile, data, summaries)
        profile.last_topic_summary_id = topic_summary.id
        profile.updated_by = "system"
        db.add(profile)
        db.flush()
        after = growth_profile_dict(profile)
        db.add(
            GrowthProfileRevision(
                user_id=user.id,
                profile_id=profile.id,
                topic_summary_id=topic_summary.id,
                before_json=json.dumps(before, ensure_ascii=False, default=str) if before else None,
                after_json=json.dumps(after, ensure_ascii=False, default=str),
                reason="recent_practice_review_v2",
            )
        )
        return profile

    @staticmethod
    def get_growth_profile(db: Session, user_id: int) -> UserGrowthProfile | None:
        return db.scalar(select(UserGrowthProfile).where(UserGrowthProfile.user_id == user_id))

    @staticmethod
    def recent_topic_summaries(db: Session, user_id: int, *, limit: int = 10) -> list[TopicSummary]:
        return list(
            db.scalars(
                select(TopicSummary)
                .where(
                    TopicSummary.user_id == user_id,
                    TopicSummary.schema_version == RECENT_REVIEW_SCHEMA_VERSION,
                )
                .order_by(TopicSummary.generated_at.desc(), TopicSummary.id.desc())
                .limit(limit)
            )
        )

    @staticmethod
    def prompt_context(db: Session, user: User) -> str | None:
        profile = GrowthProfileService.get_growth_profile(db, user.id)
        if (
            profile is None
            or profile.schema_version < RECENT_REVIEW_SCHEMA_VERSION
            or not (profile.recent_review or "").strip()
        ):
            return None
        focus = (profile.current_focus or "").strip()
        return (
            "[近期实修回顾]\n"
            "以下内容只用于保持近期对话连续性。必须优先依据用户本轮表达，不得把历史回顾当成人格、"
            "情绪、身体或关系模式，不得据此评价成长结果，也不能替代本轮可靠知识。\n"
            f"近期回顾：{_limit(profile.recent_review or '', 1400)}\n"
            + (f"近期关注：{_limit(focus, 600)}\n" if focus else "")
            + "回答时帮助用户回到当前问题和当下，不要主动复述或强化历史标签。"
        )


def topic_dict(topic: TopicSession) -> dict:
    return {
        "id": topic.id,
        "userId": topic.user_id,
        "chatSessionId": topic.chat_session_id,
        "title": topic.title,
        "coreQuestion": topic.core_question,
        "status": topic.status,
        "messageCount": topic.message_count,
        "tokenInput": topic.token_input,
        "tokenOutput": topic.token_output,
        "quotaDeducted": bool(topic.quota_deducted),
        "startedAt": topic.started_at,
        "endedAt": topic.ended_at,
        "createdAt": topic.created_at,
        "updatedAt": topic.updated_at,
    }


def topic_summary_dict(summary: TopicSummary) -> dict | None:
    if summary.schema_version < RECENT_REVIEW_SCHEMA_VERSION:
        return None
    return {
        "id": summary.id,
        "topicSessionId": summary.topic_session_id,
        "userId": summary.user_id,
        "schemaVersion": summary.schema_version,
        "summary": summary.summary,
        "currentFocus": summary.main_events,
        "nextObservation": summary.next_observation,
        "modelName": summary.model_name,
        "status": summary.status,
        "errorMessage": summary.error_message,
        "attemptCount": summary.attempt_count,
        "maxAttempts": summary.max_attempts,
        "nextRunAt": summary.next_run_at,
        "lastStartedAt": summary.last_started_at,
        "finishedAt": summary.finished_at,
        "generatedAt": summary.generated_at,
    }


def growth_profile_dict(profile: UserGrowthProfile | None) -> dict | None:
    if profile is None or profile.schema_version < RECENT_REVIEW_SCHEMA_VERSION:
        return None
    return {
        "id": profile.id,
        "userId": profile.user_id,
        "schemaVersion": profile.schema_version,
        "reviewText": profile.recent_review or "",
        "currentFocus": profile.current_focus,
        "sourceSummaryIds": _json_int_list(profile.source_summary_ids),
        "lastTopicSummaryId": profile.last_topic_summary_id,
        "updatedBy": profile.updated_by,
        "createdAt": profile.created_at,
        "updatedAt": profile.updated_at,
    }


def _apply_summary(summary: TopicSummary, data: dict[str, Any]) -> None:
    summary.summary = _field(data, "summary", "本主题已沉淀。", 4000)
    summary.main_events = _field(data, "currentFocus", "", 1200) or None
    summary.next_observation = _field(data, "nextObservation", "", 1500) or None
    summary.emotions = None
    summary.body_feelings = None
    summary.beliefs = None
    summary.recommended_homework = None
    summary.insights = None


def _apply_recent_review(
    profile: UserGrowthProfile,
    data: dict[str, Any],
    summaries: list[TopicSummary],
) -> None:
    profile.schema_version = RECENT_REVIEW_SCHEMA_VERSION
    profile.recent_review = _field(data, "reviewText", "近期还没有可回顾的主题。", 3000)
    profile.current_focus = _field(data, "currentFocus", "", 1000) or None
    profile.source_summary_ids = json.dumps([int(item.id) for item in summaries], ensure_ascii=False)
    # 主动清空旧画像字段，保证后续任何兼容代码也不会继续读取标签化内容。
    profile.profile_text = ""
    profile.recurring_topics = None
    profile.common_emotions = None
    profile.body_patterns = None
    profile.relation_patterns = None
    profile.homework_done = None
    profile.effective_homework = None
    profile.recent_progress = None


def _topic_summary_prompt(topic: TopicSession, user_content: str, assistant_context: str) -> str:
    return (
        "你是大本营千问千答的近期主题回顾助手。请输出结构化 JSON，字段仅包含："
        "summary, currentFocus, nextObservation。"
        "用户原话是唯一可以形成用户结论的证据；AI回复只用于理解上下文，不能成为用户特征或结论。"
        "只描述本次明确谈到的内容和当下关注，不分析人格、潜意识、情绪模式、身体模式、关系模式、"
        "成长阶段、功课效果或近期变化，不评分、不贴标签、不扩展课程知识。"
        "nextObservation 最多一句，使用‘可以继续留意……’的开放表达，不设目标、不追求结果。\n\n"
        f"主题标题：{topic.title}\n核心问题：{topic.core_question}\n\n"
        f"用户原话：\n{_limit(user_content, 9000)}\n\n"
        f"AI回复（仅作上下文参考）：\n{_limit(assistant_context, 3000)}"
    )


def _recent_review_prompt(summaries: list[TopicSummary]) -> str:
    evidence = [
        {
            "summaryId": item.id,
            "summary": item.summary,
            "currentFocus": item.main_events,
            "nextObservation": item.next_observation,
            "generatedAt": item.generated_at,
        }
        for item in summaries
    ]
    return (
        "你是大本营千问千答的近期实修回顾助手。请依据最近30天内、最多10条主题摘要输出 JSON，"
        "字段仅包含 reviewText 和 currentFocus。reviewText 用一小段中性文字回顾最近明确谈到的主题；"
        "currentFocus 最多列出3项近期关注。不得继承或推断长期人格、情绪模式、身体模式、关系模式，"
        "不得记录做过或有效的功课，不评价进步、变化、阶段或结果，不把一次表达固化为长期特征。"
        "使用‘最近谈到/近期关注’而不是‘你总是/你已经形成’。证据不足时直接说明近期记录较少。\n\n"
        f"近期主题摘要：\n{json.dumps(evidence, ensure_ascii=False, default=str)}"
    )


def _fallback_summary(topic: TopicSession, user_content: str, raw: str) -> dict[str, str]:
    source = user_content or raw.strip()
    user_lines = [line for line in source.splitlines() if line.startswith("用户：")]
    first_question = user_lines[0].removeprefix("用户：").strip() if user_lines else topic.core_question
    last_user_text = user_lines[-1].removeprefix("用户：").strip() if user_lines else first_question
    return {
        "summary": _limit(f"本次主要谈到：{last_user_text}", 1800),
        "currentFocus": _limit(first_question, 800),
        "nextObservation": "可以继续留意这个问题在当下实际发生时，自己最直接的体验是什么。",
    }


def _fallback_recent_review(summaries: list[TopicSummary], raw: str) -> dict[str, str]:
    usable = [item for item in summaries if item.summary.strip()]
    review_parts = [item.summary.strip() for item in usable[:3]]
    focus_parts = [item.main_events.strip() for item in usable if item.main_events and item.main_events.strip()]
    return {
        "reviewText": _limit("；".join(review_parts) or "近期沉淀记录较少。", 2600),
        "currentFocus": _limit("；".join(dict.fromkeys(focus_parts[:3])), 900),
    }


def _parse_summary_json(raw: str) -> dict[str, Any] | None:
    text = raw.strip()
    if not text:
        return None
    # 部分模型会把推理过程和最终 JSON 一起放进 answer，甚至在推理中先给出一份
    # 草稿 JSON。先移除推理块，再从后向前选取可完整解析的对象，避免把思考过程
    # 或多个 JSON 拼接后写入近期实修回顾。
    text = re.sub(r"<(?:think|analysis)>[\s\S]*?</(?:think|analysis)>", "", text, flags=re.IGNORECASE).strip()
    fenced = re.findall(r"```(?:json)?\s*([\s\S]*?)\s*```", text, flags=re.IGNORECASE)
    candidates = [*fenced, text]
    for candidate in reversed(candidates):
        parsed = _last_json_object(candidate)
        if parsed is not None:
            return parsed
    return None


def _last_json_object(text: str) -> dict[str, Any] | None:
    decoder = json.JSONDecoder()
    parsed_objects: list[dict[str, Any]] = []
    for index, character in enumerate(text):
        if character != "{":
            continue
        try:
            value, _ = decoder.raw_decode(text[index:])
        except json.JSONDecodeError:
            continue
        if isinstance(value, dict):
            parsed_objects.append(value)
    return parsed_objects[-1] if parsed_objects else None


def _messages_text(messages: list[ChatMessage], *, roles: set[str] | None = None) -> str:
    labels = {"user": "用户", "assistant": "大本营答疑助手"}
    return "\n".join(
        f"{labels.get(message.role, message.role)}：{message.content}"
        for message in messages
        if roles is None or message.role in roles
    )


def _field(data: dict[str, Any], key: str, default: str, max_len: int) -> str:
    value = data.get(key, default)
    if isinstance(value, list):
        value = "；".join(str(item) for item in value if str(item).strip())
    if not isinstance(value, str):
        value = str(value)
    return _limit(value.strip(), max_len)


def _json_int_list(raw: str | None) -> list[int]:
    if not raw:
        return []
    try:
        value = json.loads(raw)
    except json.JSONDecodeError:
        return []
    if not isinstance(value, list):
        return []
    return [int(item) for item in value if isinstance(item, int) or (isinstance(item, str) and item.isdigit())]


def _recent_review_summaries(db: Session, *, user_id: int) -> list[TopicSummary]:
    cutoff = _now() - timedelta(days=RECENT_REVIEW_DAYS)
    return list(
        db.scalars(
            select(TopicSummary)
            .where(
                TopicSummary.user_id == user_id,
                TopicSummary.schema_version == RECENT_REVIEW_SCHEMA_VERSION,
                TopicSummary.status.in_(("success", "fallback")),
                TopicSummary.generated_at >= cutoff,
            )
            .order_by(TopicSummary.generated_at.desc(), TopicSummary.id.desc())
            .limit(RECENT_REVIEW_TOPIC_LIMIT)
        )
    )


def _limit(text: str, max_len: int) -> str:
    return text[:max_len].strip()


def _now() -> datetime:
    return datetime.now(UTC).replace(tzinfo=None)


def _reset_summary_job(summary: TopicSummary) -> None:
    summary.schema_version = RECENT_REVIEW_SCHEMA_VERSION
    summary.summary = ""
    summary.main_events = None
    summary.emotions = None
    summary.body_feelings = None
    summary.beliefs = None
    summary.recommended_homework = None
    summary.insights = None
    summary.next_observation = None
    summary.model_name = None
    summary.status = "pending"
    summary.error_message = None
    summary.attempt_count = 0
    summary.next_run_at = _now()
    summary.locked_at = None
    summary.locked_by = None
    summary.last_started_at = None
    summary.finished_at = None


def _settlement_result(
    db: Session,
    *,
    topic: TopicSession,
    summary: TopicSummary,
    user: User,
    growth_enabled: bool,
) -> dict:
    profile = None
    if growth_enabled and summary.status in {"success", "fallback"}:
        profile = GrowthProfileService.get_growth_profile(db, user.id)
    return {
        "topic": topic_dict(topic),
        "summary": topic_summary_dict(summary),
        "profile": growth_profile_dict(profile),
        "growthProfileEnabled": growth_enabled,
        "settlementStatus": summary.status,
    }
