"""Shared business logic for forms, clients, grants, invites, API tokens.

Used by BOTH the dashboard/admin UI and the headless management API so the two
paths can never drift. Functions raise ValueError on bad input; callers translate.
"""

from __future__ import annotations

import secrets
from html import escape
from typing import Optional

import auth
import deps
import notify
from db import execute, query, query_one


# ---------- Forms ----------

def new_public_key() -> str:
    return secrets.token_urlsafe(8).replace("_", "").replace("-", "")[:12]


def create_form(*, name: str, creator_id: Optional[int], notify_recipients: str = "",
                redirect_url: str = "", allowed_origins: Optional[str] = None,
                client_id: Optional[int] = None) -> dict:
    name = (name or "").strip()
    if not name:
        raise ValueError("form name is required")
    key = new_public_key()
    origins = deps.DEFAULT_ALLOWED_ORIGINS if allowed_origins is None else allowed_origins.strip()
    fid = execute(
        "INSERT INTO forms (public_key, client_id, name, notify_recipients, redirect_url, "
        "allowed_origins, created_by) VALUES (?, ?, ?, ?, ?, ?, ?)",
        (key, client_id, name, notify_recipients.strip(), redirect_url.strip(), origins, creator_id),
    )
    if client_id:
        grant_form(fid, client_id)
    return {"id": fid, "public_key": key}


_UPDATABLE = {"name", "notify_recipients", "redirect_url", "allowed_origins",
              "reply_to_field", "from_address", "active"}


def update_form(form_id: int, fields: dict) -> int:
    """Update only whitelisted columns that are present in `fields`. Returns rows changed."""
    sets, params = [], []
    for col, val in fields.items():
        if col not in _UPDATABLE:
            continue
        if col == "active":
            val = 1 if str(val).lower() in ("1", "true", "yes", "on") else 0
        elif isinstance(val, str):
            val = val.strip()
        sets.append(f"{col} = ?")
        params.append(val)
    if not sets:
        raise ValueError("no updatable fields provided")
    params.append(form_id)
    return execute(f"UPDATE forms SET {', '.join(sets)} WHERE id = ?", tuple(params))


def grant_form(form_id: int, client_id: int) -> None:
    execute("INSERT OR IGNORE INTO form_access (form_id, client_id) VALUES (?, ?)",
            (form_id, client_id))


def revoke_form(form_id: int, client_id: int) -> None:
    execute("DELETE FROM form_access WHERE form_id = ? AND client_id = ?", (form_id, client_id))


def form_grantees(form_id: int):
    return query(
        "SELECT c.* FROM clients c JOIN form_access fa ON fa.client_id = c.id "
        "WHERE fa.form_id = ? ORDER BY c.name", (form_id,))


# ---------- Clients / users ----------

def find_client_by_email(email: str):
    return query_one("SELECT * FROM clients WHERE email = ?", (email.strip().lower(),))


def create_user(*, name: str, email: str, role: str = "client") -> int:
    email = (email or "").strip().lower()
    name = (name or "").strip()
    if "@" not in email:
        raise ValueError("a valid email is required")
    if role not in auth.ROLES:
        raise ValueError(f"invalid role: {role}")
    if find_client_by_email(email):
        raise ValueError("a user with that email already exists")
    return execute("INSERT INTO clients (name, email, role) VALUES (?, ?, ?)",
                   (name or email, email, role))


def invite(client_id: int, *, send_email: bool = True) -> str:
    """Notify a user they've been granted access and point them at 'Sign in with BW'.
    Returns the login URL. (No password setup — login is via BW Auth.)"""
    row = query_one("SELECT email, name FROM clients WHERE id = ?", (client_id,))
    if not row:
        raise ValueError("user not found")
    link = deps.APP_BASE_URL
    if send_email and row["email"]:
        html = (f"<p>Hi {escape(row['name'] or '')},</p>"
                f"<p>You've been given access to form submissions on SubmitStream.</p>"
                f"<p>Sign in with your BW account at "
                f"<a href='{escape(link)}'>{escape(link)}</a>. "
                f"If you don't have a BW account yet, ask your administrator to set one up.</p>")
        notify._send_via_resend(row["email"], "Your SubmitStream access", html,
                                f"Sign in with BW: {link}", deps.EMAIL_FROM, None)
    return link


# ---------- Deletion ----------

def delete_form(form_id: int) -> None:
    """Deletes the form and (via FK cascade) its submissions, notifications,
    events, and access grants."""
    execute("DELETE FROM forms WHERE id = ?", (form_id,))


def delete_submission(sub_id: int) -> None:
    execute("DELETE FROM submissions WHERE id = ?", (sub_id,))


def delete_user(client_id: int, *, acting_admin_id: Optional[int] = None) -> None:
    row = query_one("SELECT role FROM clients WHERE id = ?", (client_id,))
    if not row:
        raise ValueError("user not found")
    if row["role"] == "admin":
        raise ValueError("cannot delete an admin account")
    if acting_admin_id is not None and client_id == acting_admin_id:
        raise ValueError("cannot delete yourself")
    # Forms created_by / client_id are ON DELETE SET NULL, so forms survive.
    execute("DELETE FROM clients WHERE id = ?", (client_id,))


# ---------- API tokens ----------

def generate_api_token(client_id: int, label: str = "") -> str:
    """Create a token, store only its hash, return the plaintext (shown once)."""
    plain = auth.new_api_token()
    execute("INSERT INTO api_tokens (client_id, token_hash, label) VALUES (?, ?, ?)",
            (client_id, auth.hash_token(plain), label.strip()[:80]))
    return plain
