"""Public forms — a published form anyone with its link can fill in and
submit without signing in (FormSettings.public_access). See
app/routers/public_forms.py for the routes.

A signed-out visitor has no account, roles or session, so a public form may
only use what works without them (public_problems): no people pickers, no
embedded forms (file uploads work, through the form's own upload endpoint), no options from
a Data Query or record-sourced labels, no option filters on the signed-in
user or session, no prefill from a record, and only flow steps and events
that run in the browser alone. Choice fields linked to a datasource are
fine — their options come from a public endpoint that returns just the
fields the form shows (option_records).

Checked when the form is saved or published with public access on, and
again on every public request — a form that stopped qualifying stops being
served."""

import re
import uuid

from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.crud import licensing
from app.crud.public_site import set_tenant
from app.models.identity import Tenant
from app.models.master import MasterRecord
from app.models.workflow import Form

_SIGNED_IN_TYPES = {
    "user": "picks a person from the workspace's accounts",
    "embedded_form": "fills in another form",
}
# File fields work: a visitor uploads through the public form's own
# endpoint (POST .../files), within PUBLIC_UPLOAD_MAX_MB per file.
FILE_TYPES = {"file_upload", "image_upload", "attachment", "camera", "multi_attachment"}
PUBLIC_UPLOAD_MAX_MB = 10
# Flow steps and form events that run in the browser alone.
_PUBLIC_STEPS = {"set_field", "show_message", "confirm", "open_external_url", "set_variable", "stop"}
_PUBLIC_EVENT_ACTIONS = {"none", "show_message", "open_external_url"}
_SIGNED_IN_TOKEN = re.compile(r"\{\{\s*(current_user|session)\.")

OPTION_LIMIT = 2000


def _label(field: dict) -> str:
    return f"“{field.get('label') or field.get('name')}”"


def public_problems(field_defs: list[dict], events: list[dict] | None = None, prefill: dict | None = None, settings: dict | None = None) -> list[str]:
    """Why this form can't be filled in by someone who isn't signed in —
    one sentence each; empty when it can."""
    problems: list[str] = []

    def check(fields: list[dict]) -> None:
        for f in fields:
            kind = f.get("type")
            if kind in _SIGNED_IN_TYPES:
                problems.append(f"{_label(f)} {_SIGNED_IN_TYPES[kind]}, which needs a signed-in person.")
            if f.get("options_query_id"):
                problems.append(f"{_label(f)} takes its options from a data query, which needs a signed-in person.")
            if f.get("label_source") == "record":
                problems.append(f"{_label(f)} reads a record, which needs a signed-in person.")
            if any(_SIGNED_IN_TOKEN.search(str(o.get("value") or "")) for o in f.get("option_filters") or []):
                problems.append(f"{_label(f)} filters its options by the signed-in person or their session.")
            for step in f.get("on_change") or []:
                if step.get("type") not in _PUBLIC_STEPS:
                    problems.append(f"{_label(f)} runs “{str(step.get('type')).replace('_', ' ')}” when it changes, which needs a signed-in person.")
            if kind == "subform":
                check(f.get("subform_fields") or [])

    check(field_defs)
    for event in events or []:
        action = event.get("action_type") or "none"
        if action == "run_flow" and all(s.get("type") in _PUBLIC_STEPS for s in event.get("steps") or []):
            continue
        if action not in _PUBLIC_EVENT_ACTIONS:
            problems.append(f"The form's {str(event.get('trigger', '')).replace('_', ' ')} event runs “{action.replace('_', ' ')}”, which needs a signed-in person.")
    if prefill and prefill.get("source_id"):
        problems.append("The form is prefilled from a record, which needs a signed-in person.")
    # Anyone can type any address into a public form, so the receipt only
    # goes to one they've proved is theirs.
    receipt = (settings or {}).get("receipt") or {}
    if receipt.get("enabled"):
        to = next((f for f in field_defs if f.get("name") == receipt.get("to_field")), None)
        if to is not None and not to.get("require_verification"):
            problems.append(f"The receipt goes to {_label(to)}, which isn't verified with a code — on a public form it must be, so receipts can't be sent to other people's addresses.")
    return problems


def is_public(form: Form) -> bool:
    return bool((form.settings or {}).get("public_access"))


async def live_public_form(db: AsyncSession, tenant_id: uuid.UUID, form_id: uuid.UUID) -> tuple[Tenant, Form] | None:
    """The workspace and the form's live version when it's open to the
    public — and sets the workspace for row-level security. None otherwise
    (including a workspace that's inactive or outside its licence)."""
    tenant = await db.get(Tenant, tenant_id)
    if tenant is None or tenant.status != "active" or licensing.tenant_message(tenant):
        return None
    await set_tenant(db, tenant_id)
    form = (
        await db.execute(
            select(Form).where(Form.tenant_id == tenant_id, Form.form_id == form_id, Form.status == "published").order_by(Form.version.desc()).limit(1)
        )
    ).scalar_one_or_none()
    if form is None or not is_public(form) or public_problems(form.field_definitions, form.events, form.prefill, form.settings):
        return None
    return tenant, form


def _all_fields(field_defs: list[dict]) -> list[dict]:
    out = []
    for f in field_defs:
        out.append(f)
        if f.get("type") == "subform":
            out.extend(f.get("subform_fields") or [])
    return out


async def option_records(db: AsyncSession, tenant_id: uuid.UUID, form: Form, field_name: str) -> list[dict] | None:
    """A datasource-linked choice field's options for the public form: each
    record's id and only the fields the form needs from it (the label, the
    cascade match, the option filters'). None when there's no such field."""
    field = next((f for f in _all_fields(form.field_definitions) if f.get("name") == field_name and f.get("master_id")), None)
    if field is None:
        return None
    keep = {field.get("master_display_field"), field.get("cascade_match_field"), *(o.get("field") for o in field.get("option_filters") or [])} - {None, "", "id"}
    rows = await db.execute(
        select(MasterRecord.id, MasterRecord.field_values)
        .where(MasterRecord.tenant_id == tenant_id, MasterRecord.master_id == uuid.UUID(str(field["master_id"])), MasterRecord.is_deleted.is_(False))
        .order_by(MasterRecord.created_at)
        .limit(OPTION_LIMIT)
    )
    return [{"id": str(rid), "field_values": {k: (fv or {}).get(k) for k in keep}} for rid, fv in rows.all()]


def file_field(field_defs: list[dict], name: str) -> dict | None:
    """The form's file field `name` (a line items column too)."""
    return next((f for f in _all_fields(field_defs) if f.get("name") == name and f.get("type") in FILE_TYPES), None)
