"""
One-off repair: merge a mis-linked submission (saved under DR…/wrong initiative) onto the autosave CASE-* initiative.

Safe by default (`dry_run=True`). Does not alter application HTTP handlers — callers are scripts/operators only.

See `scripts/repair_split_submission.py`.
"""

from __future__ import annotations

import uuid
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any, Dict, Optional

from sqlalchemy import delete, select, text, update
from sqlalchemy.ext.asyncio import AsyncSession

from src.initiative_db.application_storage import upsert_artifact_full_pdf
from src.initiative_db.models import ApplicationArtifact
from src.initiative_db.models import ApplicationReviewDocument
from src.initiative_db.models import ApplicationSubmitSnapshot
from src.initiative_db.models import ApplicationTaxonomy
from src.initiative_db.models import ApplicationWorkflow
from src.initiative_db.models import Draft, Initiative


def tabs_effectively_empty(tabs: Any) -> bool:
    if not isinstance(tabs, dict) or len(tabs) == 0:
        return True
    for key in ("report", "application", "contribution"):
        val = tabs.get(key)
        if isinstance(val, dict) and len(val) > 0:
            return False
        if val:
            return False
    return True


def merge_payload_for_case_repair(
    *,
    target_case_code: str,
    good_payload: Dict[str, Any],
    bad_payload: Dict[str, Any],
) -> Dict[str, Any]:
    """
    Preserve tab JSON from whichever side still has autosave (`good` wins if non-empty),
    attach submission envelope from `bad` (submissionRecord, submissionFile).
    """
    out = dict(good_payload) if isinstance(good_payload, dict) else {}
    gp = dict(good_payload.get("tabs") or {}) if isinstance(good_payload, dict) else {}
    bp = dict(bad_payload.get("tabs") or {}) if isinstance(bad_payload, dict) else {}

    if not tabs_effectively_empty(gp):
        merged_tabs = {**gp}
    elif not tabs_effectively_empty(bp):
        merged_tabs = {**bp}
    else:
        merged_tabs = {**gp, **bp}

    out["tabs"] = merged_tabs
    out["caseId"] = target_case_code.strip()
    if isinstance(bad_payload, dict):
        if isinstance(bad_payload.get("submissionRecord"), dict):
            out["submissionRecord"] = dict(bad_payload["submissionRecord"])  # type: ignore[arg-type]
        if isinstance(bad_payload.get("submissionFile"), dict):
            out["submissionFile"] = dict(bad_payload["submissionFile"])  # type: ignore[arg-type]

    ts = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
    out["updatedAt"] = ts
    return out


@dataclass
class RepairReport:
    """Human-readable audit trail for CLI / logs."""

    dry_run: bool
    submission_record_id: str
    owner_id: str
    good_case_code: str
    bad_case_code: str
    actions: list[str] = field(default_factory=list)
    skipped: Optional[str] = None


async def _latest_draft(session: AsyncSession, initiative_id: uuid.UUID) -> Optional[Draft]:
    stmt = (
        select(Draft)
        .where(Draft.initiative_id == initiative_id)
        .order_by(Draft.updated_at.desc())
        .limit(1)
    )
    return (await session.execute(stmt)).scalar_one_or_none()


async def find_initiative_by_submission_payload_id(session: AsyncSession, submission_record_id: str) -> tuple[Optional[Initiative], Optional[Draft]]:
    """Locate initiative+draft whose `payload.submissionRecord.id` matches."""
    sid = (submission_record_id or "").strip()
    if not sid or not sid.startswith("sub-"):
        return None, None

    # PostgreSQL JSONB path (portable across SQLAlchemy JSON variants)
    stmt = (
        select(Initiative, Draft)
        .join(Draft, Draft.initiative_id == Initiative.id)
        .where(text("(drafts.payload->'submissionRecord'->>'id') = :sid"))
        .params(sid=sid)
        .order_by(Draft.updated_at.desc())
        .limit(1)
    )
    row = (await session.execute(stmt)).first()
    if row is None:
        return None, None
    return row[0], row[1]


async def resolve_good_initiative(
    session: AsyncSession,
    *,
    owner_id: uuid.UUID,
    exclude_initiative_ids: tuple[uuid.UUID, ...],
    explicit_good_case_code: Optional[str],
) -> Optional[Initiative]:
    """Prefer explicit CASE-* code; otherwise latest CASE-* initiative for this owner (with non-empty drafts preferred)."""
    from sqlalchemy import desc

    if explicit_good_case_code:
        stmt = (
            select(Initiative)
            .where(
                Initiative.owner_id == owner_id,
                Initiative.case_code == explicit_good_case_code.strip(),
            )
            .limit(1)
        )
        return (await session.execute(stmt)).scalar_one_or_none()

    stmt_all = (
        select(Initiative)
        .where(
            Initiative.owner_id == owner_id,
            Initiative.id.notin(exclude_initiative_ids),
            Initiative.case_code.ilike(r"CASE-%"),
        )
        .order_by(desc(Initiative.updated_at))
    )
    initiatives = (await session.execute(stmt_all)).scalars().all()
    nonempty_first: Optional[Initiative] = None
    for ini in initiatives:
        d = await _latest_draft(session, ini.id)
        if d is None:
            continue
        pl = dict(d.payload) if isinstance(d.payload, dict) else {}
        if not tabs_effectively_empty(pl.get("tabs")):
            nonempty_first = ini
            break
    if nonempty_first is not None:
        return nonempty_first
    return initiatives[0] if initiatives else None


async def repair_submission_cross_initiative_merge(
    session: AsyncSession,
    *,
    submission_record_id: str,
    good_case_code_explicit: Optional[str] = None,
    dry_run: bool = True,
) -> RepairReport:
    """
    Attach submission data sitting on « bad » initiative to « good » CASE-* initiative, then delete the bad initiative.

    Preconditions (enforced): same owner_id; submission id found on bad initiative only.

    Caller must ``commit()`` the session unless dry_run.
    """
    sub_id = submission_record_id.strip()
    report = RepairReport(
        dry_run=dry_run,
        submission_record_id=sub_id,
        owner_id="",
        good_case_code="",
        bad_case_code="",
        actions=["resolve bad initiative row by submissionRecord.id"],
    )

    bad_ini, bad_draft = await find_initiative_by_submission_payload_id(session, sub_id)
    if bad_ini is None or bad_draft is None:
        report.skipped = f"No initiative found with drafts.payload submissionRecord.id = {sub_id!r}"
        return report

    report.bad_case_code = bad_ini.case_code or ""
    report.owner_id = str(bad_ini.owner_id)

    good_ini = await resolve_good_initiative(
        session,
        owner_id=bad_ini.owner_id,
        exclude_initiative_ids=(bad_ini.id,),
        explicit_good_case_code=good_case_code_explicit,
    )
    if good_ini is None:
        report.skipped = "Could not resolve a target CASE-* initiative for the same owner (pass --good-case explicitly)."
        return report

    if good_ini.id == bad_ini.id:
        report.skipped = "Bad and good initiative are identical — nothing to repair."
        return report

    report.good_case_code = good_ini.case_code or ""

    good_draft = await _latest_draft(session, good_ini.id)
    if good_draft is None:
        report.skipped = "Target CASE initiative has no draft row — fix data manually."
        return report

    good_pl = dict(good_draft.payload) if isinstance(good_draft.payload, dict) else {}
    bad_pl = dict(bad_draft.payload) if isinstance(bad_draft.payload, dict) else {}
    merged = merge_payload_for_case_repair(
        target_case_code=good_ini.case_code or "",
        good_payload=good_pl,
        bad_payload=bad_pl,
    )

    if dry_run:
        report.actions.extend(
            [
                f"Would update good draft id={good_draft.id} with merged tabs + submission payload",
                f"Would set good initiative status=submitted submitted_at=max(bad, good)",
                f"Would repoint application_submit_snapshots from bad initiative {bad_ini.id} -> good {good_ini.id}",
                f"Would upsert full_pdf artifact from bad -> good",
                "Would copy application_workflow / application_taxonomy rows from bad only if missing on good",
                "Would delete application_review_documents on bad (good keeps its own review JSON)",
                f"Would DELETE initiative id={bad_ini.id} ({report.bad_case_code}); CASCADE removes orphan drafts and rows tied to bad only",
                "WARN: if evidence uploads were stored only on bad initiative, MinIO metadata rows would cascade-delete — verify in DB first",
            ]
        )
        return report

    # --- apply ---
    good_draft.payload = merged
    good_draft.version = (good_draft.version or 0) + 1
    good_ini.status = "submitted"
    if bad_ini.submitted_at and (good_ini.submitted_at is None or bad_ini.submitted_at > good_ini.submitted_at):
        good_ini.submitted_at = bad_ini.submitted_at
    elif good_ini.submitted_at is None and bad_ini.submitted_at:
        good_ini.submitted_at = bad_ini.submitted_at
    elif good_ini.submitted_at is None:
        good_ini.submitted_at = datetime.now(timezone.utc)

    await session.flush()

    await session.execute(
        update(ApplicationSubmitSnapshot)
        .where(ApplicationSubmitSnapshot.initiative_id == bad_ini.id)
        .values(initiative_id=good_ini.id)
    )

    bad_pdf = (
        await session.execute(
            select(ApplicationArtifact).where(
                ApplicationArtifact.initiative_id == bad_ini.id,
                ApplicationArtifact.role == "full_pdf",
            )
        )
    ).scalar_one_or_none()
    if bad_pdf is not None:
        await upsert_artifact_full_pdf(
            session,
            initiative_id=good_ini.id,
            storage_uri=bad_pdf.storage_uri,
            original_name=bad_pdf.original_name,
            byte_size=bad_pdf.byte_size,
            sha256_hex=bad_pdf.sha256,
            uploaded_by=bad_pdf.uploaded_by,
            storage_kind=getattr(bad_pdf, "storage_kind", None),
        )

    bad_wf = await session.get(ApplicationWorkflow, bad_ini.id)
    good_wf = await session.get(ApplicationWorkflow, good_ini.id)
    if bad_wf is not None and good_wf is None:
        session.add(
            ApplicationWorkflow(
                initiative_id=good_ini.id,
                review_status=bad_wf.review_status,
                review_deadline=bad_wf.review_deadline,
                reviewer=bad_wf.reviewer,
                supervisor=bad_wf.supervisor,
                conference=bad_wf.conference,
            )
        )
    bad_tx = await session.get(ApplicationTaxonomy, bad_ini.id)
    good_tx = await session.get(ApplicationTaxonomy, good_ini.id)
    if bad_tx is not None and good_tx is None:
        session.add(
            ApplicationTaxonomy(
                initiative_id=good_ini.id,
                subject_id=bad_tx.subject_id,
                group_id=bad_tx.group_id,
                topic_type=bad_tx.topic_type,
            )
        )

    await session.execute(delete(ApplicationReviewDocument).where(ApplicationReviewDocument.initiative_id == bad_ini.id))

    await session.delete(bad_ini)
    await session.flush()

    report.actions.extend(
        [
            f"Updated good draft {good_draft.id}, initiative {good_ini.case_code}",
            f"Repointed submit snapshots; copied full_pdf artifact; cleared review docs on bad",
            f"Deleted bad initiative {report.bad_case_code}",
        ]
    )
    return report
