"""Applications — officers review a form's submissions (FormSettings.review,
app/schemas/workflow.py::FormReview).

Each submitted submission of a form with review on has a status:

    submitted -> under_review -> returned -> resubmitted -> ... -> approved / rejected / closed

Officers set under_review / returned / approved / rejected / closed (POST
/form-submissions/<id>/review); "submitted" and "resubmitted" come from the
applicant. Returning or rejecting needs a remark, which the applicant sees.
A returned submission may be corrected by its applicant — the fields the
officer named (or all of them) — until correction_until: a signed-in
submitter through POST /form-submissions/<id>/resubmit, a public one
through the status page (app/routers/public_forms.py, /status/*).

Every change is a row in form_submission_reviews (the timeline) and an
audit log entry. The status label is copied into the form's status_field
when it has one, so lists, filters, dashboards and exports can group by it
like any answer, and the applicant is told at notify_field (email or SMS,
a background job) when it changes."""

import asyncio
import smtplib
import uuid
from datetime import datetime, timedelta, timezone

import httpx
from fastapi import HTTPException
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.core import jobs
from app.core.config import settings
from app.crud.audit import write_audit_log
from app.crud.dataset_query import _user_labels
from app.models.identity import Tenant
from app.models.workflow import Form, FormSubmission, FormSubmissionReview

LABELS = {
    "submitted": "Submitted",
    "under_review": "Under review",
    "returned": "Returned for correction",
    "resubmitted": "Resubmitted",
    "approved": "Approved",
    "rejected": "Rejected",
    "closed": "Closed",
}
NEEDS_NOTE = {"returned", "rejected"}
NOTIFY_JOB = "application.status_notice"


def review_settings(form: Form) -> dict:
    return (form.settings or {}).get("review") or {}


def enabled(form: Form) -> bool:
    return bool(review_settings(form).get("enabled"))


def current(submission: FormSubmission) -> str:
    return submission.review_status or "submitted"


def _now() -> datetime:
    return datetime.now(timezone.utc)


def _field_names(form: Form) -> set[str]:
    return {f.get("name") for f in form.field_definitions}


def _mirror(form: Form, submission: FormSubmission) -> None:
    """Copies the status label into the form's status field, if it has one."""
    name = review_settings(form).get("status_field")
    if name and name in _field_names(form):
        submission.field_values = {**(submission.field_values or {}), name: LABELS[current(submission)]}


async def _record(db: AsyncSession, form: Form, submission: FormSubmission, before: str | None, after: str, note: str | None, actor_id: uuid.UUID | None) -> None:
    submission.review_status = after
    _mirror(form, submission)
    db.add(
        FormSubmissionReview(
            tenant_id=submission.tenant_id, form_id=submission.form_id, submission_id=submission.id, from_status=before, to_status=after, note=note, actor_id=actor_id
        )
    )
    await db.flush()
    await write_audit_log(
        db, submission.tenant_id, actor_id, "submission.review", "form_submission", submission.id,
        {"form_id": str(submission.form_id), "from": before, "to": after, **({"note": note} if note else {})},
    )


async def on_submitted(db: AsyncSession, form: Form, submission: FormSubmission, actor_id: uuid.UUID | None) -> None:
    """A submission just came in (or a draft was submitted)."""
    if not enabled(form):
        return
    await _record(db, form, submission, None, "submitted", None, actor_id)


async def set_status(
    db: AsyncSession,
    form: Form,
    submission: FormSubmission,
    status: str,
    note: str | None,
    correction_fields: list[str],
    actor_id: uuid.UUID | None,
) -> FormSubmission:
    """An officer's decision. `form` is the submission's own version."""
    if submission.status != "submitted":
        raise HTTPException(status_code=409, detail="A draft can't be reviewed — it hasn't been submitted.")
    note = (note or "").strip() or None
    if status in NEEDS_NOTE and not note:
        raise HTTPException(status_code=422, detail=f"Add a remark — the applicant sees why it was {'returned' if status == 'returned' else 'rejected'}.")
    unknown = sorted(set(correction_fields) - _field_names(form))
    if unknown:
        raise HTTPException(status_code=422, detail=f"These aren't fields of this form: {', '.join(unknown)}.")
    before = current(submission)
    if before == status and not note:
        return submission
    submission.review_note = note
    submission.reviewed_at = _now()
    if status == "returned":
        days = int(review_settings(form).get("correction_days") or 14)
        submission.correction_fields = sorted(set(correction_fields)) or None
        submission.correction_until = _now() + timedelta(days=days)
    else:
        submission.correction_fields = None
        submission.correction_until = None
    await _record(db, form, submission, before, status, note, actor_id)
    await queue_notice(db, form, submission)
    return submission


def can_correct(submission: FormSubmission) -> bool:
    return (
        submission.status == "submitted"
        and not submission.is_deleted
        and current(submission) == "returned"
        and submission.correction_until is not None
        and submission.correction_until > _now()
    )


def corrected_values(submission: FormSubmission, form: Form, payload: dict) -> dict:
    """The submission's values with the applicant's changes to the fields
    they may correct — every other answer stays as it was."""
    allowed = set(submission.correction_fields or _field_names(form))
    allowed.discard(review_settings(form).get("status_field"))
    before = dict(submission.field_values or {})
    return {**before, **{k: v for k, v in (payload or {}).items() if k in allowed}}


async def on_resubmitted(db: AsyncSession, form: Form, submission: FormSubmission, actor_id: uuid.UUID | None) -> None:
    submission.correction_fields = None
    submission.correction_until = None
    await _record(db, form, submission, "returned", "resubmitted", None, actor_id)
    await queue_notice(db, form, submission)


async def history(db: AsyncSession, tenant_id: uuid.UUID, submission_id: uuid.UUID) -> list[FormSubmissionReview]:
    rows = await db.execute(
        select(FormSubmissionReview)
        .where(FormSubmissionReview.tenant_id == tenant_id, FormSubmissionReview.submission_id == submission_id)
        .order_by(FormSubmissionReview.occurred_at, FormSubmissionReview.id)
    )
    return list(rows.scalars())


async def actor_names(db: AsyncSession, tenant_id: uuid.UUID, rows: list[FormSubmissionReview]) -> dict[str, str]:
    labels = await _user_labels(db, tenant_id, {r.actor_id for r in rows} - {None})
    return {uid: (label[0] or label[1]) for uid, label in labels.items()}


# ---- Telling the applicant -------------------------------------------------------------------------


async def queue_notice(db: AsyncSession, form: Form, submission: FormSubmission) -> bool:
    name = review_settings(form).get("notify_field")
    field = next((f for f in form.field_definitions if f.get("name") == name), None)
    if field is None or field.get("type") not in ("email", "phone"):
        return False
    if not str((submission.field_values or {}).get(name) or "").strip():
        return False
    await jobs.enqueue(
        db, NOTIFY_JOB, {"submission_id": str(submission.id), "form_id": str(submission.form_id)}, tenant_id=submission.tenant_id,
        dedupe_key=f"{NOTIFY_JOB}:{submission.id}",
    )
    return True


def status_page_url(tenant_id: uuid.UUID, form_id: uuid.UUID) -> str | None:
    base = (settings.app_base_url or "").rstrip("/")
    return f"{base}/f/{tenant_id}/{form_id}/status" if base else None


def notice_text(form_name: str, reference: str | None, status: str, note: str | None, link: str | None, correction_until: datetime | None) -> str:
    lines = [f"Your application{f' {reference}' if reference else ''} to “{form_name}” is now: {LABELS.get(status, status)}."]
    if note:
        lines.append(f"Remarks: {note}")
    if status == "returned" and correction_until:
        lines.append(f"Please correct it by {correction_until.strftime('%d %b %Y')}.")
    if link:
        lines.append(f"Check it here: {link}")
    return "\n\n".join(lines)


@jobs.handler(NOTIFY_JOB, max_attempts=5)
async def _notice_job(job: jobs.Job) -> str:
    from app.crud.connectors import first_enabled_connector
    from app.crud.form_documents import reference_of
    from app.crud.secrets import connector_config
    from app.crud.tasks import _send_email_sync, send_sms

    async with jobs.tenant_session(job.tenant_id) as db:
        submission = (
            await db.execute(
                select(FormSubmission).where(FormSubmission.form_id == uuid.UUID(job.payload["form_id"]), FormSubmission.id == uuid.UUID(job.payload["submission_id"]))
            )
        ).scalar_one_or_none()
        if submission is None or submission.is_deleted:
            return "skipped — the submission is gone"
        form = (
            await db.execute(select(Form).where(Form.tenant_id == job.tenant_id, Form.form_id == submission.form_id, Form.version == submission.form_version))
        ).scalar_one()
        name = review_settings(form).get("notify_field")
        field = next((f for f in form.field_definitions if f.get("name") == name), None)
        to = str((submission.field_values or {}).get(name) or "").strip()
        if field is None or not to:
            return "skipped — no address to tell"
        channel = "email_smtp" if field.get("type") == "email" else "sms"
        connector = await first_enabled_connector(db, job.tenant_id, channel)
        if connector is None:
            raise jobs.Permanent(f"not sent — there's no enabled {'Email (SMTP)' if channel == 'email_smtp' else 'SMS'} connector")
        config = await connector_config(db, job.tenant_id, connector)
        tenant = await db.get(Tenant, job.tenant_id)
        link = status_page_url(job.tenant_id, submission.form_id) if review_settings(form).get("status_lookup") else None
        body = notice_text(form.name, reference_of(form.field_definitions, submission.field_values), current(submission), submission.review_note, link, submission.correction_until)
        subject = f"{tenant.name if tenant else 'Application'}: {form.name} — {LABELS.get(current(submission), '')}"
    try:
        if channel == "email_smtp":
            await asyncio.to_thread(_send_email_sync, config, to, subject, body)
        else:
            # Phone fields are stored as "+<dial code> <number>".
            status = await send_sms(config, to.replace(" ", ""), body[:600])
            if status >= 400:
                raise jobs.Retry(f"the SMS gateway answered HTTP {status}", delay=60 * job.attempt)
    except (smtplib.SMTPException, OSError, ValueError, httpx.HTTPError) as exc:
        raise jobs.Retry(f"couldn't send: {exc}", delay=60 * job.attempt) from None
    return "applicant told"
