"""Session data computed at every successful sign-in (POST /auth/login and
/auth/login/verify-otp), per the tenant's LoginSessionSettings
(app/schemas/identity.py). The login response carries the result; the
browser stores it as its session data, readable as "{{session.<key>}}"
everywhere a Page's on_load task result already is.

Never blocks a sign-in: a lookup that finds nothing stores null, and a
misconfigured source or a failing task is logged and skipped."""

import logging
import uuid
from typing import Any

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

from app.crud.task_history import RunTrigger
from app.crud.tasks import _match_clause, run_task
from app.models.automation import Task
from app.models.identity import Tenant, User
from app.models.master import MasterRecord
from app.models.workflow import FormSubmission
from app.schemas.identity import LOGIN_SESSION_USER_FIELDS, LoginSessionSettings, LoginSessionValue

logger = logging.getLogger(__name__)


def _user_value(user: User, field: str) -> Any:
    if field in LOGIN_SESSION_USER_FIELDS:
        value = getattr(user, field)
        return str(value) if isinstance(value, uuid.UUID) else value
    return (user.extra_fields or {}).get(field)


def _json_safe(value: Any) -> Any:
    if value is None or isinstance(value, (str, int, float, bool)):
        return value
    if isinstance(value, (list, dict)):
        return value
    return str(value)


async def _lookup(db: AsyncSession, tenant_id: uuid.UUID, spec: LoginSessionValue, match_value: Any) -> Any:
    if match_value is None or match_value == "":
        return None
    if spec.source_type == "form":
        stmt = select(FormSubmission).where(
            FormSubmission.tenant_id == tenant_id,
            FormSubmission.form_id == spec.source_id,
            FormSubmission.is_deleted.is_(False),
            FormSubmission.status == "submitted",
        )
    else:
        stmt = select(MasterRecord).where(MasterRecord.tenant_id == tenant_id, MasterRecord.master_id == spec.source_id, MasterRecord.is_deleted.is_(False))
    # Only the candidate rows — the loop below still decides.
    stmt = stmt.where(_match_clause(FormSubmission if spec.source_type == "form" else MasterRecord, spec.match_field, match_value))
    wanted = str(match_value)
    for record in (await db.execute(stmt)).scalars():
        actual = record.id if spec.match_field == "id" else (record.field_values or {}).get(spec.match_field)
        if actual is not None and str(actual) == wanted:
            return str(record.id) if spec.value_field == "id" else (record.field_values or {}).get(spec.value_field)
    return None


def _user_context(user: User) -> dict[str, Any]:
    """The same current_user.* keys a Page's context carries
    (frontend/src/lib/pageContext.ts)."""
    return {
        "current_user.id": str(user.id),
        "current_user.name": user.name,
        "current_user.email": user.email,
        "current_user.vendor_id": str(user.vendor_id) if user.vendor_id else "",
        "current_user.tenant_id": str(user.tenant_id),
        "currentUserId": str(user.id),
    }


async def build_login_session_data(
    db: AsyncSession, tenant: Tenant, user: User, settings: LoginSessionSettings | None = None
) -> dict[str, Any]:
    """`settings` defaults to the tenant's saved ones — the Settings screen's
    preview passes its unsaved draft instead."""
    if settings is None:
        try:
            settings = LoginSessionSettings.model_validate(tenant.login_session_settings or {})
        except ValueError:
            logger.warning("tenant %s has invalid login_session_settings; skipping", tenant.id)
            return {}

    data: dict[str, Any] = {}
    for spec in settings.values:
        try:
            if spec.source == "user":
                data[spec.key] = _json_safe(_user_value(user, spec.user_field or ""))
                continue
            match_value = data.get(spec.match_on[len("session."):]) if spec.match_on.startswith("session.") else _user_value(user, spec.match_on)
            data[spec.key] = _json_safe(await _lookup(db, tenant.id, spec, match_value))
        except Exception:  # never block a sign-in on one bad value
            logger.exception("login session value %r failed for user %s", spec.key, user.id)
            data[spec.key] = None

    if settings.task_id:
        task = await db.get(Task, settings.task_id)
        if task is None or task.tenant_id != tenant.id or task.status != "published":
            logger.warning("login session task %s is missing or not published; skipping", settings.task_id)
            return data
        # Each value both as "session.<key>" (the name every other task run
        # sees it under — frontend/src/lib/api.ts's sessionTaskContext) and
        # bare, as before.
        initial = {**_user_context(user), **data, **{f"session.{k}": v for k, v in data.items()}}
        try:
            # A savepoint, so a task that errors mid-way can't poison the
            # sign-in's own transaction.
            async with db.begin_nested():
                ok, _trace, context = await run_task(db, tenant.id, task, initial, trigger=RunTrigger("sign_in", started_by=user.id))
        except Exception:
            logger.exception("login session task %s failed for user %s", task.id, user.id)
            return data
        if ok:
            # Only what the task itself wrote — same rule as PageRunner.tsx's
            # on_load task.
            for key, value in context.items():
                if key not in initial or initial[key] != value:
                    data[key] = _json_safe(value)
    return data
