"""Append-only tab snapshots, submit snapshots, taxonomy/workflow projection, artifact registry."""

from __future__ import annotations

import uuid
from datetime import date, datetime, timezone
from typing import Any, Dict, List, Mapping, Optional

from sqlalchemy import desc, func, select
from sqlalchemy.dialects.postgresql import insert as pg_insert
from sqlalchemy.ext.asyncio import AsyncSession

from src.initiative_db.models import (
    ApplicationArtifact,
    ApplicationSubmitSnapshot,
    ApplicationTaxonomy,
    ApplicationWorkflow,
    DraftTabSnapshot,
    Initiative,
)

# Initiative Đơn — minh chứng (2.1 nghiên cứu / 2.2 giáo trình / kỹ thuật nhóm 1)
# Must match application_artifacts.role CHECK in 002_application_storage_extensions.sql
EVIDENCE_ROLE_RESEARCH = "research_evidence"
EVIDENCE_ROLE_TEXTBOOK = "textbook_evidence"
EVIDENCE_ROLE_TECHNICAL = "technical_evidence"

ROLE_OFFICIAL_FORM_DOCX = "official_form_docx"
ROLE_OFFICIAL_FORM_PDF = "official_form_pdf"

STORAGE_MINIO_EXPORTS = "minio_exports"
STORAGE_MINIO_ATTACHMENTS = "minio_attachments"
STORAGE_FILESYSTEM = "filesystem"
STORAGE_EXTERNAL_URL = "external_url"

VALID_DRAFT_TABS = frozenset({"report", "application", "contribution"})


def effective_storage_kind(role: str, storage_uri: str, declared: Optional[str]) -> str:
    """Resolve ``storage_kind`` from DB column or legacy ``storage_uri`` shape."""
    if declared in (
        STORAGE_MINIO_EXPORTS,
        STORAGE_MINIO_ATTACHMENTS,
        STORAGE_FILESYSTEM,
        STORAGE_EXTERNAL_URL,
    ):
        return declared
    u = (storage_uri or "").strip()
    if u.startswith("http://") or u.startswith("https://"):
        return STORAGE_EXTERNAL_URL
    if u.startswith("/submitted-initiatives/"):
        return STORAGE_FILESYSTEM
    if u.startswith("/") and not u.startswith("/initiatives/"):
        return STORAGE_FILESYSTEM
    if role in (EVIDENCE_ROLE_RESEARCH, EVIDENCE_ROLE_TEXTBOOK, EVIDENCE_ROLE_TECHNICAL):
        return STORAGE_MINIO_ATTACHMENTS
    return STORAGE_MINIO_EXPORTS


def _parse_date_only(value: Any) -> Optional[date]:
    if value is None:
        return None
    raw = str(value).strip()
    if len(raw) < 10:
        return None
    try:
        return date.fromisoformat(raw[:10])
    except ValueError:
        return None


async def record_tab_snapshot(
    session: AsyncSession,
    *,
    initiative_id: uuid.UUID,
    draft_id: Optional[uuid.UUID],
    tab: str,
    payload: Mapping[str, Any],
    source: str = "autosave",
) -> None:
    if tab not in VALID_DRAFT_TABS:
        return
    stmt = select(func.coalesce(func.max(DraftTabSnapshot.tab_version), 0)).where(
        DraftTabSnapshot.initiative_id == initiative_id,
        DraftTabSnapshot.tab == tab,
    )
    max_v = (await session.execute(stmt)).scalar()
    next_v = int(max_v or 0) + 1
    session.add(
        DraftTabSnapshot(
            initiative_id=initiative_id,
            draft_id=draft_id,
            tab=tab,
            tab_version=next_v,
            payload=dict(payload),
            source=source,
        )
    )


async def record_submit_snapshot(
    session: AsyncSession,
    *,
    initiative_id: uuid.UUID,
    submission_record_id: str,
    merged_tabs: Mapping[str, Any],
    submit_metadata: Mapping[str, Any],
    full_pdf_uri: str,
) -> None:
    session.add(
        ApplicationSubmitSnapshot(
            initiative_id=initiative_id,
            submission_record_id=submission_record_id,
            merged_tabs=dict(merged_tabs),
            submit_metadata=dict(submit_metadata),
            full_pdf_uri=full_pdf_uri,
        )
    )


async def upsert_application_taxonomy(
    session: AsyncSession,
    *,
    initiative_id: uuid.UUID,
    subject_id: str,
    group_id: str,
    topic_type: str,
) -> None:
    now = datetime.now(timezone.utc)
    stmt = pg_insert(ApplicationTaxonomy).values(
        initiative_id=initiative_id,
        subject_id=subject_id or "",
        group_id=group_id or "",
        topic_type=topic_type or "",
        updated_at=now,
    )
    stmt = stmt.on_conflict_do_update(
        index_elements=[ApplicationTaxonomy.initiative_id],
        set_={
            "subject_id": stmt.excluded.subject_id,
            "group_id": stmt.excluded.group_id,
            "topic_type": stmt.excluded.topic_type,
            "updated_at": stmt.excluded.updated_at,
        },
    )
    await session.execute(stmt)


async def upsert_application_workflow(
    session: AsyncSession,
    *,
    initiative_id: uuid.UUID,
    submission_record: Mapping[str, Any],
) -> None:
    review_status = str(submission_record.get("reviewStatus") or "not_reviewed")
    review_deadline = _parse_date_only(submission_record.get("reviewDeadline"))
    reviewer = submission_record.get("reviewer")
    supervisor = submission_record.get("supervisor")
    conference = submission_record.get("conference")
    now = datetime.now(timezone.utc)
    stmt = pg_insert(ApplicationWorkflow).values(
        initiative_id=initiative_id,
        review_status=review_status,
        review_deadline=review_deadline,
        reviewer=dict(reviewer) if isinstance(reviewer, dict) else None,
        supervisor=dict(supervisor) if isinstance(supervisor, dict) else None,
        conference=dict(conference) if isinstance(conference, dict) else None,
        updated_at=now,
    )
    stmt = stmt.on_conflict_do_update(
        index_elements=[ApplicationWorkflow.initiative_id],
        set_={
            "review_status": stmt.excluded.review_status,
            "review_deadline": stmt.excluded.review_deadline,
            "reviewer": stmt.excluded.reviewer,
            "supervisor": stmt.excluded.supervisor,
            "conference": stmt.excluded.conference,
            "updated_at": stmt.excluded.updated_at,
        },
    )
    await session.execute(stmt)


async def upsert_artifact_full_pdf(
    session: AsyncSession,
    *,
    initiative_id: uuid.UUID,
    storage_uri: str,
    original_name: Optional[str],
    byte_size: Optional[int],
    sha256_hex: Optional[str],
    uploaded_by: Optional[uuid.UUID],
    storage_kind: Optional[str] = None,
) -> None:
    now = datetime.now(timezone.utc)
    stmt = pg_insert(ApplicationArtifact).values(
        initiative_id=initiative_id,
        role="full_pdf",
        storage_uri=storage_uri,
        storage_kind=storage_kind,
        original_name=original_name,
        mime_type="application/pdf",
        byte_size=byte_size,
        sha256=sha256_hex,
        uploaded_by=uploaded_by,
        uploaded_at=now,
    )
    stmt = stmt.on_conflict_do_update(
        index_elements=[ApplicationArtifact.initiative_id, ApplicationArtifact.role],
        set_={
            "storage_uri": stmt.excluded.storage_uri,
            "storage_kind": stmt.excluded.storage_kind,
            "original_name": stmt.excluded.original_name,
            "byte_size": stmt.excluded.byte_size,
            "sha256": stmt.excluded.sha256,
            "uploaded_by": stmt.excluded.uploaded_by,
            "uploaded_at": stmt.excluded.uploaded_at,
        },
    )
    await session.execute(stmt)


async def upsert_artifact_official_form(
    session: AsyncSession,
    *,
    initiative_id: uuid.UUID,
    role: str,
    storage_uri: str,
    original_name: Optional[str],
    byte_size: Optional[int],
    sha256_hex: Optional[str],
    uploaded_by: Optional[uuid.UUID],
    mime_type: str,
    storage_kind: str = STORAGE_MINIO_EXPORTS,
) -> None:
    now = datetime.now(timezone.utc)
    stmt = pg_insert(ApplicationArtifact).values(
        initiative_id=initiative_id,
        role=role,
        storage_uri=storage_uri,
        storage_kind=storage_kind,
        original_name=original_name,
        mime_type=mime_type,
        byte_size=byte_size,
        sha256=sha256_hex,
        uploaded_by=uploaded_by,
        uploaded_at=now,
    )
    stmt = stmt.on_conflict_do_update(
        index_elements=[ApplicationArtifact.initiative_id, ApplicationArtifact.role],
        set_={
            "storage_uri": stmt.excluded.storage_uri,
            "storage_kind": stmt.excluded.storage_kind,
            "original_name": stmt.excluded.original_name,
            "byte_size": stmt.excluded.byte_size,
            "sha256": stmt.excluded.sha256,
            "uploaded_by": stmt.excluded.uploaded_by,
            "uploaded_at": stmt.excluded.uploaded_at,
            "mime_type": stmt.excluded.mime_type,
        },
    )
    await session.execute(stmt)


async def upsert_evidence_artifact(
    session: AsyncSession,
    *,
    initiative_id: uuid.UUID,
    role: str,
    storage_uri: str,
    original_name: Optional[str],
    byte_size: Optional[int],
    sha256_hex: Optional[str],
    uploaded_by: Optional[uuid.UUID],
    mime_type: str = "application/pdf",
) -> None:
    now = datetime.now(timezone.utc)
    stmt = pg_insert(ApplicationArtifact).values(
        initiative_id=initiative_id,
        role=role,
        storage_uri=storage_uri,
        storage_kind=STORAGE_MINIO_ATTACHMENTS,
        original_name=original_name,
        mime_type=mime_type,
        byte_size=byte_size,
        sha256=sha256_hex,
        uploaded_by=uploaded_by,
        uploaded_at=now,
    )
    stmt = stmt.on_conflict_do_update(
        index_elements=[ApplicationArtifact.initiative_id, ApplicationArtifact.role],
        set_={
            "storage_uri": stmt.excluded.storage_uri,
            "storage_kind": stmt.excluded.storage_kind,
            "original_name": stmt.excluded.original_name,
            "byte_size": stmt.excluded.byte_size,
            "sha256": stmt.excluded.sha256,
            "uploaded_by": stmt.excluded.uploaded_by,
            "uploaded_at": stmt.excluded.uploaded_at,
            "mime_type": stmt.excluded.mime_type,
            # Re-upload clears staff decision
            "review_status": None,
            "reviewed_by": None,
            "reviewed_at": None,
        },
    )
    await session.execute(stmt)


async def get_evidence_artifact_row(
    session: AsyncSession,
    *,
    initiative_id: uuid.UUID,
    role: str,
) -> Optional[ApplicationArtifact]:
    stmt = select(ApplicationArtifact).where(
        ApplicationArtifact.initiative_id == initiative_id,
        ApplicationArtifact.role == role,
    )
    return (await session.execute(stmt)).scalar_one_or_none()


async def set_evidence_artifact_review(
    session: AsyncSession,
    *,
    initiative_id: uuid.UUID,
    role: str,
    review_status: str,
    reviewer_id: uuid.UUID,
) -> Optional[ApplicationArtifact]:
    row = await get_evidence_artifact_row(session, initiative_id=initiative_id, role=role)
    if row is None:
        return None
    now = datetime.now(timezone.utc)
    row.review_status = review_status
    row.reviewed_by = reviewer_id
    row.reviewed_at = now
    await session.flush()
    return row


async def delete_evidence_artifact_row(
    session: AsyncSession,
    *,
    initiative_id: uuid.UUID,
    role: str,
) -> Optional[ApplicationArtifact]:
    row = await get_evidence_artifact_row(session, initiative_id=initiative_id, role=role)
    if row is None:
        return None
    await session.delete(row)
    return row


async def list_tab_snapshots_for_case(
    session: AsyncSession,
    *,
    case_code: str,
    tab: Optional[str],
    limit: int,
) -> List[Dict[str, Any]]:
    ini = (
        await session.execute(select(Initiative).where(Initiative.case_code == case_code))
    ).scalar_one_or_none()
    if ini is None:
        return []
    stmt = select(DraftTabSnapshot).where(DraftTabSnapshot.initiative_id == ini.id)
    if tab:
        stmt = stmt.where(DraftTabSnapshot.tab == tab)
    stmt = stmt.order_by(desc(DraftTabSnapshot.captured_at)).limit(max(1, min(limit, 200)))
    rows = (await session.execute(stmt)).scalars().all()
    out: List[Dict[str, Any]] = []
    for r in rows:
        out.append(
            {
                "id": str(r.id),
                "initiativeId": str(r.initiative_id),
                "draftId": str(r.draft_id) if r.draft_id else None,
                "tab": r.tab,
                "tabVersion": r.tab_version,
                "payload": r.payload,
                "source": r.source,
                "capturedAt": r.captured_at.isoformat() if r.captured_at else None,
            }
        )
    return out


async def list_submit_snapshots_for_case(
    session: AsyncSession,
    *,
    case_code: str,
    limit: int,
) -> List[Dict[str, Any]]:
    ini = (
        await session.execute(select(Initiative).where(Initiative.case_code == case_code))
    ).scalar_one_or_none()
    if ini is None:
        return []
    stmt = (
        select(ApplicationSubmitSnapshot)
        .where(ApplicationSubmitSnapshot.initiative_id == ini.id)
        .order_by(desc(ApplicationSubmitSnapshot.captured_at))
        .limit(max(1, min(limit, 50)))
    )
    rows = (await session.execute(stmt)).scalars().all()
    out: List[Dict[str, Any]] = []
    for r in rows:
        out.append(
            {
                "id": str(r.id),
                "initiativeId": str(r.initiative_id),
                "submissionRecordId": r.submission_record_id,
                "mergedTabs": r.merged_tabs,
                "submitMetadata": r.submit_metadata,
                "fullPdfUri": r.full_pdf_uri,
                "capturedAt": r.captured_at.isoformat() if r.captured_at else None,
            }
        )
    return out
