from __future__ import annotations

import json
from datetime import UTC, datetime, timedelta
from typing import Iterable, Literal
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError

from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session

from app.core.config import get_settings
from app.models.growth import PeriodicReport, TopicSummary
from app.models.user import User
from app.services.reasoning_policy_service import ReasoningPolicyService
from app.services.tracked_generation_service import TrackedGenerationService

ReportType = Literal["weekly", "monthly", "stage"]

REPORT_TYPE_LABELS = {
    "weekly": "每周实修小结",
    "monthly": "每月实修回顾",
    "stage": "阶段实修回顾",
}
REPORT_SCHEMA_VERSION = 2


class PeriodicReportService:
    @staticmethod
    def default_period(report_type: ReportType, now: datetime | None = None) -> tuple[datetime, datetime]:
        current = now or datetime.now(UTC)
        if report_type in {"weekly", "monthly"}:
            return calendar_period(
                report_type,
                current,
                get_settings().periodic_report_timezone,
                respect_schedule_delay=False,
            )
        end = current.astimezone(UTC).replace(tzinfo=None) if current.tzinfo else current
        start = end - timedelta(days=150)
        return start.replace(microsecond=0), end.replace(microsecond=0)

    @staticmethod
    def list_user_reports(
        db: Session,
        *,
        user_id: int,
        limit: int = 20,
        statuses: Iterable[str] | None = None,
    ) -> list[PeriodicReport]:
        query = select(PeriodicReport).where(
            PeriodicReport.user_id == user_id,
            PeriodicReport.schema_version == REPORT_SCHEMA_VERSION,
        )
        if statuses is not None:
            query = query.where(PeriodicReport.status.in_(tuple(statuses)))
        return list(
            db.scalars(
                query
                .order_by(PeriodicReport.period_end.desc(), PeriodicReport.id.desc())
                .limit(limit)
            )
        )

    @staticmethod
    def enqueue_for_user(
        db: Session,
        *,
        user: User,
        report_type: ReportType,
        period_start: datetime | None = None,
        period_end: datetime | None = None,
        generated_by: str = "manual",
        force: bool = True,
    ) -> PeriodicReport:
        period_start, period_end = _resolve_period(report_type, period_start, period_end)
        report = _find_report(
            db,
            user_id=user.id,
            report_type=report_type,
            period_start=period_start,
            period_end=period_end,
        )
        if report is None:
            report = _new_report(
                user=user,
                report_type=report_type,
                period_start=period_start,
                period_end=period_end,
            )
            try:
                # 同周期并发入队由唯一约束兜底，不会产生重复报告任务。
                with db.begin_nested():
                    db.add(report)
                    db.flush()
            except IntegrityError:
                report = _find_report(
                    db,
                    user_id=user.id,
                    report_type=report_type,
                    period_start=period_start,
                    period_end=period_end,
                )
                if report is None:
                    raise
        elif not force:
            return report

        if report.status == "running":
            return report
        report.title = _report_title(report_type, period_start, period_end)
        report.schema_version = REPORT_SCHEMA_VERSION
        report.status = "pending"
        report.error_message = None
        report.generated_by = generated_by
        report.attempt_count = 0
        report.max_attempts = max(1, get_settings().periodic_report_max_attempts)
        report.next_run_at = _now()
        report.locked_at = None
        report.locked_by = None
        report.last_started_at = None
        report.finished_at = None
        db.add(report)
        db.flush()
        return report

    @staticmethod
    def generate_for_user(
        db: Session,
        *,
        user: User,
        report_type: ReportType,
        period_start: datetime | None = None,
        period_end: datetime | None = None,
        generated_by: str = "manual",
    ) -> PeriodicReport:
        period_start, period_end = _resolve_period(report_type, period_start, period_end)
        report = _find_report(
            db,
            user_id=user.id,
            report_type=report_type,
            period_start=period_start,
            period_end=period_end,
        ) or _new_report(
            user=user,
            report_type=report_type,
            period_start=period_start,
            period_end=period_end,
        )
        report.generated_by = generated_by
        PeriodicReportService.generate_existing(db, report=report, user=user)
        report.finished_at = _now()
        report.next_run_at = None
        report.locked_at = None
        report.locked_by = None
        db.add(report)
        db.commit()
        db.refresh(report)
        return report

    @staticmethod
    def generate_existing(db: Session, *, report: PeriodicReport, user: User) -> PeriodicReport:
        report_type = report.report_type
        if report_type not in REPORT_TYPE_LABELS:
            raise ValueError("不支持的报告类型")
        period_start = report.period_start
        period_end = report.period_end
        report.schema_version = REPORT_SCHEMA_VERSION
        report.title = _report_title(report_type, period_start, period_end)
        summaries = _period_summaries(db, user_id=user.id, period_start=period_start, period_end=period_end)
        topic_ids = sorted({int(item.topic_session_id) for item in summaries})
        summary_ids = [int(item.id) for item in summaries]
        report.source_topic_ids = json.dumps(topic_ids, ensure_ascii=False)
        report.source_summary_ids = json.dumps(summary_ids, ensure_ascii=False)
        report.generated_at = _now()

        if not summaries:
            report.status = "empty"
            report.error_message = None
            report.content = _empty_report_content(report_type=report_type, period_start=period_start, period_end=period_end)
            db.add(report)
            return report

        try:
            prompt = _report_prompt(
                user=user,
                report_type=report_type,
                period_start=period_start,
                period_end=period_end,
                summaries=summaries,
            )
            completion = TrackedGenerationService.generate(
                db,
                prompt=prompt,
                scenario="report",
                user_id=user.id,
            )
            report.content = ReasoningPolicyService.strip_reasoning(completion.answer).strip() or _fallback_report(summaries)
            report.model_name = completion.model_name
            report.status = "success"
            report.error_message = None
        except Exception as exc:
            report.status = "failed"
            report.error_message = str(exc)[:2000]
            report.content = _fallback_report(summaries)
        db.add(report)
        return report


def periodic_report_dict(report: PeriodicReport) -> dict:
    return {
        "id": report.id,
        "userId": report.user_id,
        "schemaVersion": report.schema_version,
        "reportType": report.report_type,
        "reportTypeLabel": REPORT_TYPE_LABELS.get(report.report_type, report.report_type),
        "periodStart": _local_datetime(report.period_start),
        "periodEnd": _local_datetime(report.period_end),
        "title": report.title,
        "content": report.content,
        "sourceSummaryIds": _parse_json_list(report.source_summary_ids),
        "sourceTopicIds": _parse_json_list(report.source_topic_ids),
        "modelName": report.model_name,
        "status": report.status,
        "errorMessage": report.error_message,
        "generatedBy": report.generated_by,
        "attemptCount": report.attempt_count,
        "maxAttempts": report.max_attempts,
        "nextRunAt": report.next_run_at,
        "lastStartedAt": report.last_started_at,
        "finishedAt": report.finished_at,
        "generatedAt": report.generated_at,
        "createdAt": report.created_at,
        "updatedAt": report.updated_at,
    }


def calendar_period(
    report_type: str,
    now_utc: datetime,
    timezone_name: str = "Asia/Shanghai",
    *,
    respect_schedule_delay: bool,
) -> tuple[datetime, datetime]:
    """计算最近一个完整自然周或自然月，返回 UTC 无时区时间。"""

    try:
        timezone = ZoneInfo(timezone_name)
    except ZoneInfoNotFoundError:
        timezone = ZoneInfo("Asia/Shanghai")
    aware_utc = now_utc.replace(tzinfo=UTC) if now_utc.tzinfo is None else now_utc.astimezone(UTC)
    local_now = aware_utc.astimezone(timezone)

    if report_type == "weekly":
        period_end_local = (local_now - timedelta(days=local_now.weekday())).replace(
            hour=0,
            minute=0,
            second=0,
            microsecond=0,
        )
        if respect_schedule_delay and local_now < period_end_local + timedelta(hours=2):
            period_end_local -= timedelta(days=7)
        period_start_local = period_end_local - timedelta(days=7)
    elif report_type == "monthly":
        period_end_local = local_now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
        if respect_schedule_delay and local_now < period_end_local + timedelta(hours=3):
            period_end_local = _previous_month_start(period_end_local)
        period_start_local = _previous_month_start(period_end_local)
    else:
        raise ValueError("不支持的自然周期报告类型")

    return (
        period_start_local.astimezone(UTC).replace(tzinfo=None),
        period_end_local.astimezone(UTC).replace(tzinfo=None),
    )


def _find_report(
    db: Session,
    *,
    user_id: int,
    report_type: ReportType,
    period_start: datetime,
    period_end: datetime,
) -> PeriodicReport | None:
    return db.scalar(
        select(PeriodicReport).where(
            PeriodicReport.user_id == user_id,
            PeriodicReport.report_type == report_type,
            PeriodicReport.period_start == period_start,
            PeriodicReport.period_end == period_end,
        )
    )


def _new_report(
    *,
    user: User,
    report_type: ReportType,
    period_start: datetime,
    period_end: datetime,
) -> PeriodicReport:
    return PeriodicReport(
        user_id=user.id,
        schema_version=REPORT_SCHEMA_VERSION,
        report_type=report_type,
        period_start=period_start,
        period_end=period_end,
        title=_report_title(report_type, period_start, period_end),
    )


def _resolve_period(
    report_type: ReportType,
    period_start: datetime | None,
    period_end: datetime | None,
) -> tuple[datetime, datetime]:
    if report_type not in REPORT_TYPE_LABELS:
        raise ValueError("不支持的报告类型")
    if period_start is None or period_end is None:
        default_start, default_end = PeriodicReportService.default_period(report_type)
        period_start = period_start or default_start
        period_end = period_end or default_end
    start = period_start.replace(tzinfo=None, microsecond=0)
    end = period_end.replace(tzinfo=None, microsecond=0)
    if start >= end:
        raise ValueError("报告开始时间必须早于结束时间")
    return start, end


def _report_title(report_type: str, period_start: datetime, period_end: datetime) -> str:
    local_start = _local_datetime(period_start)
    local_end = _local_datetime(period_end)
    return f"{REPORT_TYPE_LABELS[report_type]}（{local_start:%Y-%m-%d} 至 {local_end:%Y-%m-%d}）"


def _previous_month_start(value: datetime) -> datetime:
    if value.month == 1:
        return value.replace(year=value.year - 1, month=12, day=1)
    return value.replace(month=value.month - 1, day=1)


def _period_summaries(db: Session, *, user_id: int, period_start: datetime, period_end: datetime) -> list[TopicSummary]:
    return list(
        db.scalars(
            select(TopicSummary)
            .where(
                TopicSummary.user_id == user_id,
                TopicSummary.schema_version == REPORT_SCHEMA_VERSION,
                TopicSummary.status == "success",
                TopicSummary.generated_at >= period_start,
                TopicSummary.generated_at < period_end,
            )
            .order_by(TopicSummary.generated_at.asc(), TopicSummary.id.asc())
            .limit(200)
        )
    )


def _report_prompt(
    *,
    user: User,
    report_type: ReportType,
    period_start: datetime,
    period_end: datetime,
    summaries: list[TopicSummary],
) -> str:
    local_start = _local_datetime(period_start)
    local_end = _local_datetime(period_end)
    summary_text = "\n\n".join(
        (
            f"主题摘要 {index}\n"
            f"摘要：{item.summary}\n"
            f"近期关注：{item.main_events or '无'}\n"
            f"下一步观察：{item.next_observation or '无'}"
        )
        for index, item in enumerate(summaries, start=1)
    )
    return (
        f"请为学员“{user.name or user.nickname or user.phone}”生成一份{REPORT_TYPE_LABELS[report_type]}。\n"
        f"周期：{local_start:%Y-%m-%d %H:%M} 至 {local_end:%Y-%m-%d %H:%M}。\n\n"
        "产品定位：这是帮助学员回到当下的近期实修回顾，不是成长成绩单、医疗诊断或心理咨询结论。\n"
        "只依据本周期主题摘要，使用 Markdown 输出：本周期谈到的主题、近期关注、可以继续留意的问题。\n"
        "不要统计或归纳情绪、身体、关系模式，不列做过或有效的功课，不评价进步、变化、阶段和结果，"
        "不设置下一阶段目标。使用‘最近谈到/可以继续留意’，避免‘你总是/你已经形成’。\n"
        "如果证据不足，请明确说“本周期沉淀记录较少”，不要编造。\n\n"
        f"本周期主题摘要：\n{summary_text}"
    )


def _fallback_report(summaries: list[TopicSummary]) -> str:
    lines = ["## 本周期实修回顾", "", "以下内容根据本周期已沉淀的主题整理："]
    for item in summaries[:20]:
        lines.extend(["", f"- {item.summary}"])
        if item.next_observation:
            lines.append(f"  - 可以继续留意：{item.next_observation}")
    return "\n".join(lines)


def _empty_report_content(*, report_type: ReportType, period_start: datetime, period_end: datetime) -> str:
    local_start = _local_datetime(period_start)
    local_end = _local_datetime(period_end)
    return (
        f"## {REPORT_TYPE_LABELS[report_type]}\n\n"
        f"周期：{local_start:%Y-%m-%d} 至 {local_end:%Y-%m-%d}\n\n"
        "本周期还没有可用于生成报告的主题沉淀。可以在完成一次主题对话后，先点击“沉淀本主题”，再生成报告。"
    )


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


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


def _local_datetime(value: datetime) -> datetime:
    try:
        timezone = ZoneInfo(get_settings().periodic_report_timezone)
    except ZoneInfoNotFoundError:
        timezone = ZoneInfo("Asia/Shanghai")
    aware_utc = value.replace(tzinfo=UTC) if value.tzinfo is None else value.astimezone(UTC)
    return aware_utc.astimezone(timezone).replace(tzinfo=None)
