from __future__ import annotations

import hashlib
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
from typing import Any
from uuid import uuid4

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

from app.api.deps import CurrentUserContext
from app.models.attachment_association_job import AttachmentAssociationJob
from app.schemas.attachment_association_job import (
    AttachmentAssociationJobCreate,
    AttachmentAssociationJobRead,
)

JOB_LEASE_SECONDS = 300


@dataclass(slots=True)
class ClaimedAttachmentAssociationJob:
    job_id: str
    receipt_ids: list[str]
    current_user: CurrentUserContext
    attempt_count: int


def create_persistent_job(
    db: Session,
    payload: AttachmentAssociationJobCreate,
    current_user: CurrentUserContext,
) -> AttachmentAssociationJobRead:
    tenant_id = normalize_tenant_id(current_user.tenant_id)
    owner_username = str(current_user.username or current_user.name or "").strip()
    receipt_ids = list(payload.receipt_ids)
    dedupe_key = build_job_dedupe_key(receipt_ids)
    existing = db.scalar(
        _latest_generation_query(
            tenant_id=tenant_id,
            owner_username=owner_username,
            dedupe_key=dedupe_key,
        )
    )
    if existing is not None and _can_reuse_job(existing):
        return job_to_read(existing)

    generation = int(existing.generation or 0) + 1 if existing is not None else 1
    job = AttachmentAssociationJob(
        id=f"attachment-association-{uuid4()}",
        tenant_id=tenant_id,
        owner_username=owner_username,
        owner_name=str(current_user.name or "").strip(),
        owner_context_json=serialize_current_user(current_user),
        dedupe_key=dedupe_key,
        generation=generation,
        receipt_ids_json=receipt_ids,
        prompt=str(payload.prompt or "").strip(),
        conversation_id=str(payload.conversation_id or "").strip(),
        status="queued",
        message="已创建附件关联任务，等待后台处理。",
    )
    db.add(job)
    try:
        db.commit()
    except IntegrityError:
        db.rollback()
        existing = db.scalar(
            _latest_generation_query(
                tenant_id=tenant_id,
                owner_username=owner_username,
                dedupe_key=dedupe_key,
            )
        )
        if existing is None:
            raise
        return job_to_read(existing)
    db.refresh(job)
    return job_to_read(job)


def get_authorized_persistent_job(
    db: Session,
    job_id: str,
    current_user: CurrentUserContext,
) -> AttachmentAssociationJob | None:
    job = db.get(AttachmentAssociationJob, str(job_id or "").strip())
    if job is None or job.tenant_id != normalize_tenant_id(current_user.tenant_id):
        return None
    if current_user.is_admin:
        return job
    return job if job.owner_username == str(current_user.username or "").strip() else None


def claim_persistent_job(
    db: Session,
    job_id: str,
) -> ClaimedAttachmentAssociationJob | None:
    job = db.scalar(
        select(AttachmentAssociationJob)
        .where(AttachmentAssociationJob.id == str(job_id or "").strip())
        .with_for_update()
    )
    if job is None or job.status in {"succeeded", "failed"}:
        db.rollback()
        return None
    now = datetime.now(UTC)
    lease_expires_at = as_utc(job.lease_expires_at)
    if job.status == "running" and lease_expires_at is not None and lease_expires_at > now:
        db.rollback()
        return None
    job.status = "running"
    job.message = "正在匹配费用事件和可关联草稿..."
    job.attempt_count = int(job.attempt_count or 0) + 1
    job.lease_expires_at = now + timedelta(seconds=JOB_LEASE_SECONDS)
    job.updated_at = now
    claimed = ClaimedAttachmentAssociationJob(
        job_id=job.id,
        receipt_ids=[str(item) for item in list(job.receipt_ids_json or []) if str(item)],
        current_user=deserialize_current_user(job.owner_context_json),
        attempt_count=job.attempt_count,
    )
    db.commit()
    return claimed


def update_persistent_job(
    db: Session,
    job_id: str,
    *,
    expected_attempt_count: int,
    **updates: Any,
) -> None:
    job = db.scalar(
        select(AttachmentAssociationJob)
        .where(
            AttachmentAssociationJob.id == str(job_id or "").strip(),
            AttachmentAssociationJob.attempt_count == expected_attempt_count,
            AttachmentAssociationJob.status == "running",
        )
        .with_for_update()
    )
    if job is None:
        db.rollback()
        return
    for key, value in updates.items():
        if hasattr(job, key):
            setattr(job, key, value)
    job.lease_expires_at = None
    job.updated_at = datetime.now(UTC)
    db.commit()


def job_to_read(job: AttachmentAssociationJob) -> AttachmentAssociationJobRead:
    return AttachmentAssociationJobRead(
        job_id=job.id,
        status=job.status,
        message=job.message,
        receipt_ids=list(job.receipt_ids_json or []),
        claim_id=job.claim_id,
        claim_no=job.claim_no,
        uploaded_count=job.uploaded_count,
        skipped_count=job.skipped_count,
        resolution=job.resolution,
        requires_confirmation=job.requires_confirmation,
        expense_case_id=job.expense_case_id,
        application_claim_id=job.application_claim_id,
        application_claim_no=job.application_claim_no,
        confidence=job.confidence,
        confidence_score=job.confidence_score,
        match_reasons=list(job.match_reasons_json or []),
        exceptions=list(job.exceptions_json or []),
        missing_fields=list(job.missing_fields_json or []),
        risk_items=list(job.risk_items_json or []),
        candidates=list(job.candidates_json or []),
        draft_payload=dict(job.draft_payload_json) if job.draft_payload_json else None,
        error=job.error,
        prompt=job.prompt,
        conversation_id=job.conversation_id,
        created_at=job.created_at,
        updated_at=job.updated_at,
    )


def _latest_generation_query(
    *,
    tenant_id: str,
    owner_username: str,
    dedupe_key: str,
):
    return (
        select(AttachmentAssociationJob)
        .where(
            AttachmentAssociationJob.tenant_id == tenant_id,
            AttachmentAssociationJob.owner_username == owner_username,
            AttachmentAssociationJob.dedupe_key == dedupe_key,
        )
        .order_by(
            AttachmentAssociationJob.generation.desc(),
            AttachmentAssociationJob.created_at.desc(),
        )
        .limit(1)
    )


def _can_reuse_job(job: AttachmentAssociationJob) -> bool:
    if job.status in {"queued", "running"}:
        return True
    return (
        job.status == "succeeded"
        and not bool(job.requires_confirmation)
        and str(job.resolution or "").strip() == "auto_associated"
    )


def build_job_dedupe_key(receipt_ids: list[str]) -> str:
    normalized = "\n".join(sorted({str(item or "").strip() for item in receipt_ids if item}))
    return hashlib.sha256(normalized.encode("utf-8")).hexdigest()


def normalize_tenant_id(value: str | None) -> str:
    return str(value or "default").strip() or "default"


def as_utc(value: datetime | None) -> datetime | None:
    if value is None:
        return None
    return value.replace(tzinfo=UTC) if value.tzinfo is None else value.astimezone(UTC)


def serialize_current_user(current_user: CurrentUserContext) -> dict[str, Any]:
    return {
        "username": current_user.username,
        "name": current_user.name,
        "role_codes": list(current_user.role_codes or []),
        "is_admin": bool(current_user.is_admin),
        "tenant_id": normalize_tenant_id(current_user.tenant_id),
        "department_name": current_user.department_name,
        "cost_center": current_user.cost_center,
        "position": current_user.position,
        "grade": current_user.grade,
        "employee_no": current_user.employee_no,
        "manager_name": current_user.manager_name,
        "employee_id": current_user.employee_id,
        "auth_session_id": current_user.auth_session_id,
    }


def deserialize_current_user(payload: dict[str, Any] | None) -> CurrentUserContext:
    data = dict(payload or {})
    return CurrentUserContext(
        username=str(data.get("username") or "").strip(),
        name=str(data.get("name") or "").strip(),
        role_codes=[str(item) for item in list(data.get("role_codes") or []) if str(item)],
        is_admin=bool(data.get("is_admin")),
        tenant_id=normalize_tenant_id(str(data.get("tenant_id") or "default")),
        department_name=str(data.get("department_name") or ""),
        cost_center=str(data.get("cost_center") or ""),
        position=str(data.get("position") or ""),
        grade=str(data.get("grade") or ""),
        employee_no=str(data.get("employee_no") or ""),
        manager_name=str(data.get("manager_name") or ""),
        employee_id=str(data.get("employee_id") or ""),
        auth_session_id=str(data.get("auth_session_id") or ""),
    )
