from __future__ import annotations

import hashlib
import re
from datetime import UTC, date, datetime, timedelta
from typing import Any

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

from app.models.tenant import Tenant
from app.models.tenant_finance_report import (
    TenantFinanceReportConfig,
    TenantFinanceReportRun,
)

EMAIL_PATTERN = re.compile(r"^[^\s@]+@[^\s@]+\.[^\s@]+$")
RUN_STALE_AFTER = timedelta(hours=2)


def require_report_tenant_id(value: object) -> str:
    tenant_id = str(value or "").strip()
    if not tenant_id:
        raise ValueError("tenant_id 不能为空。")
    return tenant_id


def normalize_report_recipients(values: list[str] | None) -> list[str]:
    recipients: list[str] = []
    for raw in list(values or []):
        for item in str(raw or "").replace(";", ",").split(","):
            email = item.strip().lower()
            if email and EMAIL_PATTERN.fullmatch(email) and email not in recipients:
                recipients.append(email)
    return recipients


class TenantFinanceReportConfigService:
    def __init__(self, db: Session) -> None:
        self.db = db

    def get(self, *, tenant_id: str) -> TenantFinanceReportConfig | None:
        tenant = require_report_tenant_id(tenant_id)
        return self.db.scalar(
            select(TenantFinanceReportConfig).where(TenantFinanceReportConfig.tenant_id == tenant)
        )

    def configured_recipients(
        self,
        *,
        tenant_id: str,
        requested: list[str] | None = None,
    ) -> list[str]:
        config = self.get(tenant_id=tenant_id)
        if config is None or config.status != "active" or not config.delivery_enabled:
            return []
        configured = normalize_report_recipients(
            [str(item) for item in list(config.recipients_json or [])]
        )
        if not configured:
            return []
        if requested is None:
            return configured
        requested_values = normalize_report_recipients(requested)
        # 手工触发也只能缩小到本租户已配置的收件人，不能把数据外发到任意地址。
        return [item for item in requested_values if item in configured]

    def upsert(
        self,
        *,
        tenant_id: str,
        recipients: list[str],
        delivery_enabled: bool,
        updated_by: str,
    ) -> TenantFinanceReportConfig:
        tenant = require_report_tenant_id(tenant_id)
        if self.db.get(Tenant, tenant) is None:
            raise LookupError("租户不存在。")
        normalized = normalize_report_recipients(recipients)
        if delivery_enabled and not normalized:
            raise ValueError("启用财务报告投递前必须配置至少一个有效收件邮箱。")
        row = self.get(tenant_id=tenant)
        if row is None:
            row = TenantFinanceReportConfig(tenant_id=tenant)
        row.recipients_json = normalized
        row.delivery_enabled = bool(delivery_enabled)
        row.status = "active" if delivery_enabled else "disabled"
        row.updated_by = str(updated_by or "").strip()[:100]
        self.db.add(row)
        self.db.commit()
        self.db.refresh(row)
        return row


class TenantFinanceReportRunService:
    def __init__(self, db: Session) -> None:
        self.db = db

    def reserve(
        self,
        *,
        tenant_id: str,
        report_type: str,
        period_start: date,
        period_end: date,
    ) -> tuple[TenantFinanceReportRun, bool]:
        tenant = require_report_tenant_id(tenant_id)
        key = self.idempotency_key(
            tenant_id=tenant,
            report_type=report_type,
            period_start=period_start,
            period_end=period_end,
        )
        existing = self._get(tenant, key, for_update=True)
        if existing is not None:
            if existing.status == "failed" or self._is_stale(existing):
                existing.status = "running"
                existing.error_message = None
                existing.started_at = datetime.now(UTC)
                existing.finished_at = None
                self.db.commit()
                self.db.refresh(existing)
                return existing, True
            return existing, False

        row = TenantFinanceReportRun(
            tenant_id=tenant,
            report_type=report_type,
            period_start=period_start,
            period_end=period_end,
            idempotency_key=key,
            status="running",
        )
        self.db.add(row)
        try:
            self.db.commit()
            self.db.refresh(row)
            return row, True
        except IntegrityError:
            self.db.rollback()
            winner = self._get(tenant, key, for_update=False)
            if winner is None:
                raise
            return winner, False

    def attach_agent_run(self, row: TenantFinanceReportRun, run_id: str) -> None:
        row.agent_run_id = str(run_id or "").strip() or None
        self.db.add(row)
        self.db.commit()

    def succeed(self, row: TenantFinanceReportRun, result: dict[str, Any]) -> None:
        row.status = "succeeded"
        row.storage_key = str((result.get("pdf") or {}).get("storage_key") or "")
        row.result_json = dict(result)
        row.error_message = None
        row.finished_at = datetime.now(UTC)
        self.db.add(row)
        self.db.commit()

    def fail(self, row: TenantFinanceReportRun, exc: Exception) -> None:
        row.status = "failed"
        row.error_message = str(exc)[:2000]
        row.finished_at = datetime.now(UTC)
        self.db.add(row)
        self.db.commit()

    @staticmethod
    def idempotency_key(
        *,
        tenant_id: str,
        report_type: str,
        period_start: date,
        period_end: date,
    ) -> str:
        canonical = ":".join(
            (
                require_report_tenant_id(tenant_id),
                str(report_type or "").strip(),
                period_start.isoformat(),
                period_end.isoformat(),
            )
        )
        return "finance-report:v1:" + hashlib.sha256(canonical.encode()).hexdigest()

    def _get(
        self,
        tenant_id: str,
        key: str,
        *,
        for_update: bool,
    ) -> TenantFinanceReportRun | None:
        stmt = select(TenantFinanceReportRun).where(
            TenantFinanceReportRun.tenant_id == tenant_id,
            TenantFinanceReportRun.idempotency_key == key,
        )
        if for_update:
            stmt = stmt.with_for_update()
        return self.db.scalar(stmt)

    @staticmethod
    def _is_stale(row: TenantFinanceReportRun) -> bool:
        if row.status != "running" or row.started_at is None:
            return False
        started = row.started_at
        if started.tzinfo is None:
            started = started.replace(tzinfo=UTC)
        return datetime.now(UTC) - started > RUN_STALE_AFTER
