"""Public, unauthenticated endpoints: the submit receiver, embed.js, health.

SECURITY: the notification recipient is read ONLY from forms.notify_recipients
(server-side). The POST body never controls who gets emailed — that would make
this an open relay. The submitter's email is used only as Reply-To.
"""

from __future__ import annotations

import json

from fastapi import APIRouter, Request
from fastapi.responses import FileResponse, JSONResponse, RedirectResponse, Response

import deps
import notify
import security
from db import execute, query_one

router = APIRouter()


@router.get("/health")
def health():
    return {"ok": True, "service": "submitstream", "version": deps.jinja_env.globals["app_version"]}


@router.get("/embed.js")
def embed_js():
    path = deps.STATIC_DIR / "embed.js"
    return FileResponse(path, media_type="application/javascript",
                        headers={"Cache-Control": "public, max-age=600"})


def _wants_json(request: Request) -> bool:
    if "application/json" in request.headers.get("accept", ""):
        return True
    return request.headers.get("x-requested-with", "").lower() in ("fetch", "xmlhttprequest")


async def _parse_payload(request: Request) -> dict:
    ct = request.headers.get("content-type", "")
    if ct.startswith("application/json"):
        raw = await request.body()
        if len(raw) > deps.MAX_BODY_BYTES:
            raise ValueError("body too large")
        data = json.loads(raw or b"{}")
        if not isinstance(data, dict):
            raise ValueError("json body must be an object")
        return data
    form = await request.form()
    out: dict = {}
    for key, val in form.multi_items():
        sval = val if isinstance(val, str) else getattr(val, "filename", "")  # files unsupported in v1
        if len(out) >= deps.MAX_FIELDS:
            break
        sval = sval[: deps.MAX_FIELD_LEN]
        if key in out:  # collapse repeats (checkboxes) into a list
            existing = out[key]
            out[key] = existing + [sval] if isinstance(existing, list) else [existing, sval]
        else:
            out[key] = sval
    return out


@router.options("/f/{public_key}")
async def submit_preflight(public_key: str, request: Request):
    form = query_one("SELECT allowed_origins FROM forms WHERE public_key = ? AND active = 1",
                     (public_key,))
    allowed = form["allowed_origins"] if form else ""
    origin = security.request_origin(request)
    return Response(status_code=204, headers=security.cors_headers(origin, allowed))


@router.post("/f/{public_key}")
async def submit(public_key: str, request: Request):
    form = query_one("SELECT * FROM forms WHERE public_key = ? AND active = 1", (public_key,))
    if not form:
        return JSONResponse({"ok": False, "error": "unknown form"}, status_code=404)

    fid = form["id"]
    ip = security.client_ip(request)
    origin = security.request_origin(request)
    allowed = form["allowed_origins"]
    cors = security.cors_headers(origin, allowed)

    def _ok_response(payload: dict | None = None):
        if _wants_json(request):
            return JSONResponse({"ok": True}, headers=cors)
        # Priority: page-supplied `_redirect` (same-origin guarded) → form setting → /thanks.
        target = None
        candidate = (payload or {}).get("_redirect")
        if candidate:
            target = security.safe_redirect(candidate, origin, allowed)
            if not target:
                deps.log_event(f"ignored unsafe _redirect: {candidate}", form_id=fid, level="warn")
        target = target or form["redirect_url"] or f"{deps.APP_BASE_URL}/thanks"
        return RedirectResponse(target, status_code=303)

    # 1) Origin allowlist (no-op in learn mode).
    if not security.origin_allowed(origin, allowed):
        deps.log_event(f"rejected: origin {origin} not in allowlist", form_id=fid,
                       level="warn", data={"ip": ip})
        return JSONResponse({"ok": False, "error": "origin not allowed"}, status_code=403,
                            headers=cors)

    # 2) Rate limit (per form + IP).
    if security.rate_limited(fid, ip, form["rate_limit_per_hour"]):
        deps.log_event(f"rejected: rate limit ({form['rate_limit_per_hour']}/h) from {ip}",
                       form_id=fid, level="warn")
        return JSONResponse({"ok": False, "error": "rate limited"}, status_code=429, headers=cors)

    # 3) Parse body.
    try:
        payload = await _parse_payload(request)
    except (ValueError, json.JSONDecodeError) as exc:
        deps.log_event(f"rejected: bad body ({exc})", form_id=fid, level="warn")
        return JSONResponse({"ok": False, "error": "invalid body"}, status_code=400, headers=cors)

    honeypot = form["honeypot_field"]
    is_spam = security.honeypot_tripped(payload, honeypot) or \
        security.too_fast(payload, form["min_submit_seconds"])

    clean = {k: v for k, v in payload.items() if not k.startswith("_") and k != honeypot}

    # 4) Store the submission (always — spam included, marked).
    sub_id = execute(
        "INSERT INTO submissions (form_id, payload_json, submitter_ip, user_agent, origin, status) "
        "VALUES (?, ?, ?, ?, ?, ?)",
        (fid, json.dumps(clean), ip, request.headers.get("user-agent", "")[:500],
         origin, "spam" if is_spam else "new"),
    )
    deps.log_event("received submission", submission_id=sub_id, form_id=fid,
                   data={"ip": ip, "origin": origin, "fields": list(clean.keys())})

    if is_spam:
        deps.log_event("flagged as spam (honeypot/timing) — no notification sent",
                       submission_id=sub_id, form_id=fid, level="warn")
        return _ok_response(payload)  # look identical to success

    # 5) Learn mode: record the origin we saw so it can be locked down later.
    if not security.parse_allowed(allowed) and origin:
        deps.log_event(f"learn-mode origin observed: {origin}", submission_id=sub_id, form_id=fid)

    # 6) Enqueue notifications (recipients are SERVER-SIDE only).
    recipients = [r.strip() for r in form["notify_recipients"].split(",") if r.strip()]
    if not recipients:
        deps.log_event("no recipients configured — entry stored but nobody notified",
                       submission_id=sub_id, form_id=fid, level="warn")
    for rcpt in recipients:
        nid = notify.enqueue_notification(sub_id, rcpt, "notify")
        notify.process_notification(nid)  # try immediately; worker retries failures

    # 7) Optional autoresponse to the submitter (off by default).
    if form["autoresponse_enabled"]:
        rt = clean.get(form["reply_to_field"])
        if isinstance(rt, str) and "@" in rt:
            nid = notify.enqueue_notification(sub_id, rt, "autoresponse")
            notify.process_notification(nid)

    return _ok_response(payload)
