import uuid

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

from app.core.database import get_db
from app.core.permissions import deny_vendor, require_permission
from app.core.security import AuthContext, get_auth_context
from app.crud import account_emails, license_files, license_requests, licensing, sign_in_policy
from app.crud.portal_menu import check_logo_file
from app.crud.audit import write_audit_log
from app.crud.login_session import build_login_session_data
from app.crud.user_verification import VerificationError, require_connector, verification_channels
from app.models.automation import Task
from app.models.identity import Tenant, User
from app.models.master import Master
from app.models.navigation import Page
from app.models.workflow import Form
from app.schemas.identity import LoginSessionPreview, LoginSessionSettings, PortalMenuSettings, RegistrationFieldsUpdate, TenantLicenseStatus, TenantRead, TenantTheme
from app.schemas.identity import LicenseFileCheck, LicenseFileInput, LicenseRequestCreate, LicenseRequestRead
from app.schemas.public_site import SiteSettings, SiteSettingsRead, SiteSettingsUpdate

router = APIRouter(prefix="/tenants", tags=["tenants"])



@router.get("/me", response_model=TenantRead)
async def get_my_tenant(
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
) -> Tenant:
    """
    The `tenants` table has no tenant_id column of its own, so unlike every
    other table it carries no Row-Level Security policy (see db/schema.sql).
    This endpoint is deliberately the ONLY route onto that table, and it
    always filters explicitly by the caller's own X-Tenant-Id — never expose
    a bare list-all-tenants route here, since nothing at the database layer
    would stop it from leaking every tenant's name/status cross-tenant.
    """
    result = await db.execute(select(Tenant).where(Tenant.id == ctx.tenant_id))
    tenant = result.scalar_one_or_none()
    if tenant is None:
        raise HTTPException(status_code=404, detail="Tenant not found")
    return tenant


@router.patch("/me", response_model=TenantRead)
async def update_my_tenant_registration_fields(
    payload: RegistrationFieldsUpdate,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("settings", "edit")),
    __: AuthContext = Depends(deny_vendor()),
) -> Tenant:
    """Tenant Admin self-service config for the Public Portal registration
    form (Register.tsx) — the only thing a tenant admin can edit about their
    own tenant. Renaming/suspending the tenant itself stays Super-Admin-only
    (PATCH /admin/tenants/{id}), which is why this takes the narrower
    RegistrationFieldsUpdate rather than TenantUpdate."""
    result = await db.execute(select(Tenant).where(Tenant.id == ctx.tenant_id))
    tenant = result.scalar_one_or_none()
    if tenant is None:
        raise HTTPException(status_code=404, detail="Tenant not found")
    # Full re-dump per field (not a bare exclude_unset dump of the payload) so
    # every field def is stored with its complete shape — same rationale as
    # routers/tasks.py's create/update: an omitted optional key (e.g. a
    # text field's absent `options`) must land as an explicit null/default,
    # not simply missing, since the frontend editor assumes the full shape.
    tenant.registration_field_schema = [f.model_dump(mode="json") for f in payload.registration_field_schema]
    if payload.registration_settings is not None:
        # Every new registrant would be stuck unverifiable without a way to
        # deliver the email/SMS, so the connector must exist up front.
        try:
            for channel in verification_channels(payload.registration_settings.verification):
                await require_connector(db, ctx.tenant_id, channel)
        except VerificationError as exc:
            raise HTTPException(status_code=exc.status_code, detail=str(exc)) from None
        tenant.registration_settings = payload.registration_settings.model_dump(mode="json")
    await db.flush()
    await db.refresh(tenant)
    return tenant


async def _check_login_session_refs(db: AsyncSession, tenant_id: uuid.UUID, payload: LoginSessionSettings) -> None:
    """Every datasource and the task must exist in this tenant (RLS already
    hides other tenants' rows, so "not found" covers both)."""
    for v in payload.values:
        if v.source != "datasource":
            continue
        model = Form if v.source_type == "form" else Master
        column = Form.form_id if v.source_type == "form" else Master.id
        found = (await db.execute(select(model).where(model.tenant_id == tenant_id, column == v.source_id).limit(1))).scalar_one_or_none()
        if found is None:
            raise HTTPException(status_code=422, detail=f"Session value '{v.key}': that {'form' if v.source_type == 'form' else 'datasource'} no longer exists.")
    if payload.task_id:
        task = (await db.execute(select(Task).where(Task.tenant_id == tenant_id, Task.id == payload.task_id))).scalar_one_or_none()
        if task is None:
            raise HTTPException(status_code=422, detail="The selected task no longer exists.")
        if task.status != "published":
            raise HTTPException(status_code=422, detail=f"Publish the task \"{task.name}\" first — only published tasks run after login.")


@router.get("/me/license", response_model=TenantLicenseStatus)
async def get_my_license(
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("users_roles", "view")),
    __: AuthContext = Depends(deny_vendor()),
) -> TenantLicenseStatus:
    """The workspace's license and the seats in use (app/crud/licensing.py).
    Read-only here — only the Super Admin changes it."""
    tenant = await db.get(Tenant, ctx.tenant_id)
    return await licensing.license_status(db, tenant)


@router.post("/me/license-file/check", response_model=LicenseFileCheck)
async def check_license_file(
    payload: LicenseFileInput,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("settings", "edit")),
    __: AuthContext = Depends(deny_vendor()),
) -> LicenseFileCheck:
    """What a license file from the Super Admin would change
    (app/crud/license_files.py) — nothing is changed yet."""
    tenant = await db.get(Tenant, ctx.tenant_id)
    try:
        _, checked = await license_files.check(db, tenant, payload.content)
    except license_files.LicenseFileError as exc:
        raise license_files.http_error(exc) from None
    return checked


@router.post("/me/license-file/apply", response_model=TenantLicenseStatus)
async def apply_license_file(
    payload: LicenseFileInput,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("settings", "edit")),
    __: AuthContext = Depends(deny_vendor()),
) -> TenantLicenseStatus:
    """Makes a license file's terms the workspace's license."""
    tenant = await db.get(Tenant, ctx.tenant_id)
    try:
        await license_files.apply(db, tenant, payload.content, ctx.user_id)
    except license_files.LicenseFileError as exc:
        raise license_files.http_error(exc) from None
    return await licensing.license_status(db, tenant)


@router.get("/me/license-requests", response_model=list[LicenseRequestRead])
async def list_license_requests(
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("users_roles", "view")),
    __: AuthContext = Depends(deny_vendor()),
) -> list[LicenseRequestRead]:
    """Renewal requests to the Super Admin (app/crud/license_requests.py), newest first."""
    return await license_requests.history(db, ctx.tenant_id)


@router.post("/me/license-requests", response_model=LicenseRequestRead, status_code=201)
async def request_license(
    payload: LicenseRequestCreate,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("settings", "edit")),
    __: AuthContext = Depends(deny_vendor()),
) -> LicenseRequestRead:
    try:
        request = await license_requests.create(db, ctx.tenant_id, ctx.user_id, payload)
    except license_files.LicenseFileError as exc:
        raise license_files.http_error(exc) from None
    return next(r for r in await license_requests.history(db, ctx.tenant_id) if r.id == request.id)


@router.post("/me/license-requests/{request_id}/cancel", status_code=204)
async def cancel_license_request(
    request_id: uuid.UUID,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("settings", "edit")),
    __: AuthContext = Depends(deny_vendor()),
) -> None:
    try:
        await license_requests.cancel(db, ctx.tenant_id, request_id, ctx.user_id)
    except license_files.LicenseFileError as exc:
        raise license_files.http_error(exc) from None


@router.post("/me/license-requests/{request_id}/apply", response_model=TenantLicenseStatus)
async def apply_requested_license(
    request_id: uuid.UUID,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("settings", "edit")),
    __: AuthContext = Depends(deny_vendor()),
) -> TenantLicenseStatus:
    """Applies the license the Super Admin issued for a request."""
    tenant = await db.get(Tenant, ctx.tenant_id)
    try:
        await license_requests.apply(db, tenant, request_id, ctx.user_id)
    except license_files.LicenseFileError as exc:
        raise license_files.http_error(exc) from None
    return await licensing.license_status(db, tenant)


@router.get("/me/email-settings", response_model=account_emails.EmailSettingsRead)
async def get_email_settings(
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("settings", "view")),
    __: AuthContext = Depends(deny_vendor()),
) -> account_emails.EmailSettingsRead:
    """The workspace's own account emails and their templates (app/crud/account_emails.py)."""
    return await account_emails.read(db, await db.get(Tenant, ctx.tenant_id))


@router.put("/me/email-settings", response_model=account_emails.EmailSettingsRead)
async def update_email_settings(
    payload: account_emails.EmailSettings,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("settings", "edit")),
    __: AuthContext = Depends(deny_vendor()),
) -> account_emails.EmailSettingsRead:
    tenant = await db.get(Tenant, ctx.tenant_id)
    try:
        await account_emails.write(db, tenant, payload)
    except ValueError as exc:
        raise HTTPException(status_code=422, detail=str(exc)) from None
    await write_audit_log(db, ctx.tenant_id, ctx.user_id, "tenant.email_settings_updated", "tenant", ctx.tenant_id)
    await db.flush()
    return await account_emails.read(db, tenant)


class EmailTestRequest(BaseModel):
    template: account_emails.TemplateKey
    # Unsaved edits to try; omitted = what's saved.
    settings: account_emails.EmailSettings | None = None


class EmailTestResult(BaseModel):
    result: str


@router.post("/me/email-settings/test", response_model=EmailTestResult)
async def send_test_email(
    payload: EmailTestRequest,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("settings", "edit")),
    __: AuthContext = Depends(deny_vendor()),
) -> EmailTestResult:
    """Sends one template, with sample values, to the person asking."""
    tenant = await db.get(Tenant, ctx.tenant_id)
    me = await db.get(User, ctx.user_id) if ctx.user_id else None
    if me is None:
        raise HTTPException(status_code=400, detail="A test email goes to your own address — sign in as a user.")
    return EmailTestResult(result=await account_emails.send_test(db, tenant, payload.template, me, payload.settings))


@router.get("/me/security", response_model=sign_in_policy.SecuritySettingsRead)
async def get_my_security_settings(
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("settings", "view")),
    __: AuthContext = Depends(deny_vendor()),
) -> sign_in_policy.SecuritySettingsRead:
    """Sign-in policy and single sign-on (app/crud/sign_in_policy.py). The
    provider's client secret is never sent back — only whether one is set."""
    from app.routers.auth import sso_redirect_uri

    tenant = await db.get(Tenant, ctx.tenant_id)
    return sign_in_policy.to_read(sign_in_policy.settings_of(tenant), sso_redirect_uri())


@router.put("/me/security", response_model=sign_in_policy.SecuritySettingsRead)
async def update_my_security_settings(
    payload: sign_in_policy.SecuritySettingsWrite,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("settings", "edit")),
    __: AuthContext = Depends(deny_vendor()),
) -> sign_in_policy.SecuritySettingsRead:
    from app.models.identity import Role
    from app.routers.auth import sso_redirect_uri

    tenant = await db.get(Tenant, ctx.tenant_id)
    if payload.sso.default_role_id:
        role = await db.get(Role, uuid.UUID(payload.sso.default_role_id))
        if role is None or role.tenant_id != ctx.tenant_id or role.scope != "internal":
            raise HTTPException(status_code=422, detail="Choose one of this workspace's internal roles for new SSO users.")
    current = sign_in_policy.settings_of(tenant)
    updated = sign_in_policy.apply_write(current, payload, session_used_sso="sso" in ctx.amr)
    tenant.security_settings = updated.model_dump(mode="json")
    changes = {k: v for k, v in payload.model_dump(mode="json").items() if k != "sso"}
    changes["sso"] = {k: v for k, v in payload.sso.model_dump(mode="json").items() if k != "client_secret"}
    changes["sso"]["client_secret_changed"] = payload.sso.client_secret is not None
    await write_audit_log(db, ctx.tenant_id, ctx.user_id, "tenant.security.update", "tenant", ctx.tenant_id, changes)
    await db.flush()
    return sign_in_policy.to_read(updated, sso_redirect_uri())


@router.patch("/me/login-session", response_model=TenantRead)
async def update_my_login_session_settings(
    payload: LoginSessionSettings,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("settings", "edit")),
    __: AuthContext = Depends(deny_vendor()),
) -> Tenant:
    """What to compute into every user's session data at sign-in — see
    app/crud/login_session.py. A full replace."""
    tenant = (await db.execute(select(Tenant).where(Tenant.id == ctx.tenant_id))).scalar_one_or_none()
    if tenant is None:
        raise HTTPException(status_code=404, detail="Tenant not found")
    await _check_login_session_refs(db, ctx.tenant_id, payload)
    tenant.login_session_settings = payload.model_dump(mode="json")
    await db.flush()
    await db.refresh(tenant)
    return tenant


@router.post("/me/login-session/preview", response_model=LoginSessionPreview)
async def preview_login_session(
    payload: LoginSessionSettings,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("settings", "edit")),
    __: AuthContext = Depends(deny_vendor()),
) -> LoginSessionPreview:
    """Runs the (possibly unsaved) settings for the calling admin, so the
    Settings screen can show what a sign-in would store. The optional task
    really runs, side effects included."""
    tenant = (await db.execute(select(Tenant).where(Tenant.id == ctx.tenant_id))).scalar_one_or_none()
    user = await db.get(User, ctx.user_id) if ctx.user_id else None
    if tenant is None or user is None:
        raise HTTPException(status_code=404, detail="Not found")
    await _check_login_session_refs(db, ctx.tenant_id, payload)
    return LoginSessionPreview(session_data=await build_login_session_data(db, tenant, user, payload))


@router.patch("/me/theme", response_model=TenantRead)
async def update_my_theme(
    payload: TenantTheme,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("settings", "edit")),
    __: AuthContext = Depends(deny_vendor()),
) -> Tenant:
    """The tenant's brand color, font and corner radius — applied for every
    signed-in user (Studio and Portal). A full replace."""
    tenant = (await db.execute(select(Tenant).where(Tenant.id == ctx.tenant_id))).scalar_one_or_none()
    if tenant is None:
        raise HTTPException(status_code=404, detail="Tenant not found")
    tenant.theme = payload.model_dump(mode="json", exclude_none=True)
    await db.flush()
    await db.refresh(tenant)
    await write_audit_log(db, ctx.tenant_id, ctx.user_id, "tenant.theme", "tenant", tenant.id, tenant.theme)
    return tenant


@router.patch("/me/portal-menu", response_model=TenantRead)
async def update_my_portal_menu_settings(
    payload: PortalMenuSettings,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("menus", "edit")),
    __: AuthContext = Depends(deny_vendor()),
) -> Tenant:
    """Tenant Admin config for the Public Portal navigation menu's look and
    behaviour (position, item style, colors, tooltips, search, expand/
    collapse). A full replace — the Portal appearance editor always sends the
    whole object — stored as a complete dump so every key lands explicitly.
    Read back by every portal user through GET /tenants/me."""
    result = await db.execute(select(Tenant).where(Tenant.id == ctx.tenant_id))
    tenant = result.scalar_one_or_none()
    if tenant is None:
        raise HTTPException(status_code=404, detail="Tenant not found")
    if payload.logo_file_id is not None:
        await check_logo_file(db, ctx.tenant_id, payload.logo_file_id)
    tenant.portal_menu_settings = payload.model_dump(mode="json")
    await db.flush()
    await db.refresh(tenant)
    return tenant


# ---- The public website -------------------------------------------------------------------------------------

async def _site_read(db: AsyncSession, tenant: Tenant) -> SiteSettingsRead:

    cfg = SiteSettings.model_validate(tenant.site_settings or {})
    home = None
    if cfg.home_page_id:
        home = (await db.execute(select(Page).where(Page.tenant_id == tenant.id, Page.id == cfg.home_page_id))).scalar_one_or_none()
    return SiteSettingsRead(
        **cfg.model_dump(),
        slug=tenant.site_slug,
        path=f"/site/{tenant.site_slug}/" if tenant.site_slug else None,
        home_page_name=home.name if home else None,
        home_page_live=bool(home and home.is_public and home.slug and home.published_version is not None),
    )


@router.get("/me/site", response_model=SiteSettingsRead)
async def get_my_site(
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("settings", "view")),
    __: AuthContext = Depends(deny_vendor()),
) -> SiteSettingsRead:
    tenant = (await db.execute(select(Tenant).where(Tenant.id == ctx.tenant_id))).scalar_one_or_none()
    if tenant is None:
        raise HTTPException(status_code=404, detail="Tenant not found")
    return await _site_read(db, tenant)


@router.put("/me/site", response_model=SiteSettingsRead)
async def update_my_site(
    payload: SiteSettingsUpdate,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("settings", "edit")),
    __: AuthContext = Depends(deny_vendor()),
) -> SiteSettingsRead:
    """The workspace's public website (app/routers/public_site.py): its
    address, landing page, default SEO and domains. A full replace."""

    tenant = (await db.execute(select(Tenant).where(Tenant.id == ctx.tenant_id))).scalar_one_or_none()
    if tenant is None:
        raise HTTPException(status_code=404, detail="Tenant not found")
    if payload.enabled and not payload.slug:
        raise HTTPException(status_code=422, detail="Give the website an address before turning it on.")
    if payload.slug:
        # The tenants table has no row-level security; only the clash is read.
        taken = (await db.execute(select(Tenant.id).where(Tenant.site_slug == payload.slug, Tenant.id != tenant.id))).scalar_one_or_none()
        if taken is not None:
            raise HTTPException(status_code=409, detail=f"The address /site/{payload.slug}/ is taken — pick another.")
    for host in payload.domains:
        taken = (
            await db.execute(select(Tenant.id).where(Tenant.site_settings["domains"].contains([host]), Tenant.id != tenant.id).limit(1))
        ).scalar_one_or_none()
        if taken is not None:
            raise HTTPException(status_code=409, detail=f"{host} is already another workspace's domain.")
    if payload.home_page_id:
        home = (await db.execute(select(Page).where(Page.tenant_id == tenant.id, Page.id == payload.home_page_id))).scalar_one_or_none()
        if home is None:
            raise HTTPException(status_code=422, detail="The landing page doesn't exist.")
        if not home.is_public:
            raise HTTPException(status_code=422, detail=f"“{home.name}” isn't public yet — put it on the website from the Page Designer first.")
    before = {"slug": tenant.site_slug, **(tenant.site_settings or {})}
    tenant.site_slug = payload.slug
    tenant.site_settings = SiteSettings.model_validate(payload.model_dump(exclude={"slug"})).model_dump(mode="json")
    await db.flush()
    await db.refresh(tenant)
    await write_audit_log(db, ctx.tenant_id, ctx.user_id, "tenant.site", "tenant", tenant.id, {"before": before, "after": {"slug": tenant.site_slug, **tenant.site_settings}})
    return await _site_read(db, tenant)
