"""Public (signed-out) read access to datasources and forms — see
app/crud/public_data.py.

Studio (signed in):
    GET /public-data                              every source's public access
    GET /public-data/<master|form>/<id>           one source's, with its offerable fields
    PUT /public-data/<master|form>/<id>           set it (edit permission on datasources / forms)

Public (no session; the workspace's public website must be on):
    GET  /public/sites/<site>/data/<master|form>/<id>/schema   the public fields
    POST /public/sites/<site>/data/query                       a data block's rows
    POST /public/sites/<site>/data/series                      a chart's series"""

import uuid
from datetime import datetime, timezone

from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.core import throttle
from app.core.database import AsyncSessionLocal, get_db, limit_statement_time
from app.core.permissions import deny_vendor, resolve_action_matrix
from app.core.security import AuthContext, get_auth_context
from app.crud import public_data
from app.crud import public_site as site_crud
from app.crud.audit import write_audit_log
from app.models.public_data import PublicDataSource
from app.schemas.public_data import (
    PublicDataConfig,
    PublicDataConfigRead,
    PublicDataQuery,
    PublicDataResult,
    PublicDataSourceType,
    PublicField,
    PublicSeriesQuery,
    PublicSeriesResult,
    PublicSourceSchema,
)

router = APIRouter(tags=["public data"])

READ_LIMIT = (300, 60.0)  # per address and site, a minute


class OfferableField(BaseModel):
    name: str
    label: str
    type: str


class PublicDataConfigDetail(PublicDataConfigRead):
    # Every field that could be made public (files, signatures and passwords never can).
    offerable: list[OfferableField] = []


_MODULE = {"master": "masters", "form": "forms"}


async def _require(db: AsyncSession, ctx: AuthContext, source_type: str, action: str) -> None:
    matrix, admin = await resolve_action_matrix(db, ctx.tenant_id, ctx.user_id)
    if not admin and action not in matrix.get(_MODULE[source_type], []):
        raise HTTPException(status_code=403, detail=f"Your role doesn't have '{action}' access to {_MODULE[source_type]}.")


def _read(row: PublicDataSource | None, source_type: str, source_id: uuid.UUID) -> PublicDataConfigRead:
    if row is None:
        return PublicDataConfigRead(source_type=source_type, source_id=source_id)
    return PublicDataConfigRead(
        source_type=row.source_type, source_id=row.source_id, enabled=row.enabled, fields=row.fields, filters=row.filters, allow_search=row.allow_search, updated_at=row.updated_at
    )


@router.get("/public-data", response_model=list[PublicDataConfigRead])
async def list_public_data(
    ctx: AuthContext = Depends(get_auth_context), db: AsyncSession = Depends(get_db), _: AuthContext = Depends(deny_vendor())
) -> list[PublicDataConfigRead]:
    rows = (await db.execute(select(PublicDataSource).where(PublicDataSource.tenant_id == ctx.tenant_id))).scalars().all()
    return [_read(r, r.source_type, r.source_id) for r in rows]


@router.get("/public-data/{source_type}/{source_id}", response_model=PublicDataConfigDetail)
async def get_public_data(
    source_type: PublicDataSourceType,
    source_id: uuid.UUID,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(deny_vendor()),
) -> PublicDataConfigDetail:
    await _require(db, ctx, source_type, "view")
    found = await public_data.source_fields(db, ctx.tenant_id, source_type, source_id)
    if found is None:
        raise HTTPException(status_code=404, detail="Not found")
    _name, defs = found
    offerable = [OfferableField(name=f["name"], label=public_data.field_label(source_type, f), type=f.get("type") or "") for f in defs if public_data.offerable(source_type, f)]
    offerable += [OfferableField(name="created_at", label="Created", type="datetime"), OfferableField(name="updated_at", label="Updated", type="datetime")]
    read = _read(await public_data.get_config(db, ctx.tenant_id, source_type, source_id), source_type, source_id)
    return PublicDataConfigDetail(**read.model_dump(), offerable=offerable)


@router.put("/public-data/{source_type}/{source_id}", response_model=PublicDataConfigRead)
async def set_public_data(
    source_type: PublicDataSourceType,
    source_id: uuid.UUID,
    payload: PublicDataConfig,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(deny_vendor()),
) -> PublicDataConfigRead:
    """Who can see what, without signing in. Takes effect at once — a public
    page's data blocks read through it on every request."""
    await _require(db, ctx, source_type, "edit")
    found = await public_data.source_fields(db, ctx.tenant_id, source_type, source_id)
    if found is None:
        raise HTTPException(status_code=404, detail="Not found")
    filters = [f.model_dump() for f in payload.filters]
    fields = list(dict.fromkeys(payload.fields))
    problems = public_data.check_config(source_type, found[1], fields, filters)
    if payload.enabled and not fields:
        problems.append("Choose at least one field to show.")
    if problems:
        raise HTTPException(status_code=422, detail={"code": "public_data", "message": " ".join(problems), "problems": problems})
    row = await public_data.get_config(db, ctx.tenant_id, source_type, source_id)
    before = _read(row, source_type, source_id).model_dump(mode="json", exclude={"updated_at"})
    if row is None:
        row = PublicDataSource(tenant_id=ctx.tenant_id, source_type=source_type, source_id=source_id)
        db.add(row)
    row.enabled, row.fields, row.filters, row.allow_search = payload.enabled, fields, filters, payload.allow_search
    row.updated_at, row.updated_by = datetime.now(timezone.utc), ctx.user_id
    await db.flush()
    await write_audit_log(
        db, ctx.tenant_id, ctx.user_id, "public_data.updated", source_type, source_id, {"before": before, "after": payload.model_dump(mode="json")}
    )
    return _read(row, source_type, source_id)


# ---- Public ----------------------------------------------------------------------------------------


async def _site(db: AsyncSession, slug: str):
    site = await site_crud.site_by_slug(db, slug)
    if site is None:
        raise HTTPException(status_code=404, detail="This site isn't available.")
    return site


@router.get("/public/sites/{slug}/data/{source_type}/{source_id}/schema", response_model=PublicSourceSchema)
async def public_schema(slug: str, source_type: PublicDataSourceType, source_id: uuid.UUID, request: Request) -> PublicSourceSchema:
    throttle.check(f"public-data:{slug}:{throttle.client_ip(request)}", *READ_LIMIT)
    async with AsyncSessionLocal() as db, db.begin():
        await limit_statement_time(db)
        site = await _site(db, slug)
        try:
            config, name, shown = await public_data.open_source(db, site.tenant.id, source_type, source_id)
        except public_data.PublicDataError as exc:
            raise public_data.http_error(exc) from None
        return PublicSourceSchema(
            source_type=source_type,
            source_id=source_id,
            name=name,
            fields=[PublicField(**public_data.presented(source_type, f)) for f in shown],
            allow_search=config.allow_search,
        )


@router.post("/public/sites/{slug}/data/query", response_model=PublicDataResult)
async def public_query(slug: str, payload: PublicDataQuery, request: Request) -> PublicDataResult:
    throttle.check(f"public-data:{slug}:{throttle.client_ip(request)}", *READ_LIMIT)
    async with AsyncSessionLocal() as db, db.begin():
        await limit_statement_time(db)
        site = await _site(db, slug)
        try:
            rows, total, columns = await public_data.query(db, site.tenant.id, payload.model_dump(mode="json"))
        except public_data.PublicDataError as exc:
            raise public_data.http_error(exc) from None
        return PublicDataResult(columns=columns, rows=rows, total=total)


@router.post("/public/sites/{slug}/data/series", response_model=PublicSeriesResult)
async def public_series(slug: str, payload: PublicSeriesQuery, request: Request) -> PublicSeriesResult:
    throttle.check(f"public-data:{slug}:{throttle.client_ip(request)}", *READ_LIMIT)
    async with AsyncSessionLocal() as db, db.begin():
        await limit_statement_time(db)
        site = await _site(db, slug)
        try:
            rows, series = await public_data.series(db, site.tenant.id, payload.model_dump(mode="json"))
        except public_data.PublicDataError as exc:
            raise public_data.http_error(exc) from None
        return PublicSeriesResult(rows=rows, series=series)
