"""
Admin custom page builder: layout JSON per slug (rows/columns + widget map).
Requires admin.custom_pages for mutations and layout reads under /api/admin/custom-pages.

Layout version 2: rows of columns (12-column grid spans) + widgets map. Version 1 (flat widget list) is migrated on read/write.
"""
import json
import logging
import re
import uuid
from datetime import datetime
from typing import Any, Dict, List, Literal, Optional

from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field, field_validator, model_validator
from sqlalchemy.orm import Session

from app.database import get_db
from app.models import CustomPageLayout, Permission, User
from app.routers.auth import require_permission

logger = logging.getLogger(__name__)

router = APIRouter(prefix="/api/admin/custom-pages", tags=["admin"])

LAYOUT_SLUG_DEFAULT = "default"

SLUG_PATTERN = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
RESERVED_SLUGS = frozenset(
    {
        "api",
        "auth",
        "static",
        "assets",
        "docs",
        "health",
        "layout",
        "branches",
        "overview",
        "bi",
        "company",
        "directors",
        "admin",
        "custom-pages",
        "page",
        "login",
        "set-password",
    }
)

ALLOWED_WIDGET_TYPES = frozenset(
    {
        "date_filter",
        "revenue_snapshot",
        "cash_in_bank",
        "wip_summary",
        "quotes_summary",
        "google_reviews",
        "content_note",
        "section_heading",
        "divider",
        "subscriber_totals",
        "time_utilisation",
        "marketing_snapshot",
    }
)


def _normalize_view_permission_key(db: Session, key: Optional[str]) -> Optional[str]:
    """Return None for public-to-any-logged-in-user; else validate key exists in permissions table."""
    if key is None:
        return None
    s = (key or "").strip()
    if not s:
        return None
    found = db.query(Permission).filter(Permission.key == s).first()
    if not found:
        raise HTTPException(status_code=400, detail=f"Unknown permission key: {s}")
    return s


def normalize_slug(slug: str) -> str:
    s = (slug or "").strip().lower()
    if not s or len(s) > 64:
        raise HTTPException(status_code=400, detail="Invalid slug")
    if not SLUG_PATTERN.match(s):
        raise HTTPException(
            status_code=400,
            detail="Slug must be lowercase letters, numbers, and single hyphens between segments",
        )
    if s in RESERVED_SLUGS:
        raise HTTPException(status_code=400, detail="Reserved slug")
    return s


def _new_id(prefix: str) -> str:
    return f"{prefix}-{uuid.uuid4().hex[:12]}"


class WidgetInstance(BaseModel):
    id: str = Field(..., min_length=1, max_length=80)
    type: str = Field(..., min_length=1, max_length=64)
    title: Optional[str] = Field(None, max_length=200)
    config: Optional[Dict[str, Any]] = None

    @field_validator("type")
    @classmethod
    def type_must_be_allowed(cls, v: str) -> str:
        if v not in ALLOWED_WIDGET_TYPES:
            raise ValueError(f"Unknown widget type: {v}. Allowed: {sorted(ALLOWED_WIDGET_TYPES)}")
        return v


class LayoutColumn(BaseModel):
    id: str = Field(..., min_length=1, max_length=80)
    span: int = Field(12, ge=1, le=12)
    widget_ids: List[str] = Field(default_factory=list)


class LayoutRow(BaseModel):
    id: str = Field(..., min_length=1, max_length=80)
    columns: List[LayoutColumn] = Field(min_length=1)


class LayoutBodyV1(BaseModel):
    """Legacy flat list of widgets (version 1)."""

    version: Literal[1] = 1
    widgets: List[WidgetInstance] = Field(default_factory=list)

    @field_validator("widgets")
    @classmethod
    def unique_widget_ids_v1(cls, widgets: List[WidgetInstance]) -> List[WidgetInstance]:
        ids = [w.id for w in widgets]
        if len(ids) != len(set(ids)):
            raise ValueError("Widget ids must be unique")
        return widgets


class LayoutBodyV2(BaseModel):
    version: Literal[2] = 2
    widgets: Dict[str, WidgetInstance] = Field(default_factory=dict)
    rows: List[LayoutRow] = Field(default_factory=list)

    @model_validator(mode="after")
    def validate_v2_grid(self) -> "LayoutBodyV2":
        if not self.widgets and not self.rows:
            return self
        if self.widgets and not self.rows:
            raise ValueError("Layout has widgets but no rows")

        col_ids: set = set()
        for row in self.rows:
            if not row.columns:
                raise ValueError(f"Row {row.id} has no columns")
            span_sum = sum(c.span for c in row.columns)
            if span_sum != 12:
                raise ValueError(f"Each row's column spans must sum to 12; row {row.id} sums to {span_sum}")
            for col in row.columns:
                if col.id in col_ids:
                    raise ValueError(f"Duplicate column id: {col.id}")
                col_ids.add(col.id)

        referenced: List[str] = []
        for row in self.rows:
            for col in row.columns:
                referenced.extend(col.widget_ids)

        keys = set(self.widgets.keys())
        for wid in referenced:
            if wid not in keys:
                raise ValueError(f"Unknown widget id referenced in layout: {wid}")
        for wid in keys:
            if wid not in referenced:
                raise ValueError(f"Widget {wid} is not placed in any column")
        if len(referenced) != len(keys):
            raise ValueError("Each widget must appear exactly once in the grid")
        if sorted(referenced) != sorted(keys):
            raise ValueError("Each widget must appear exactly once in the grid")

        return self


def migrate_v1_to_v2(body: LayoutBodyV1) -> Dict[str, Any]:
    """Stack legacy widgets in a single full-width column."""
    widgets_dict = {w.id: w.model_dump() for w in body.widgets}
    if not body.widgets:
        return {"version": 2, "widgets": {}, "rows": []}
    row_id = _new_id("row")
    col_id = _new_id("col")
    return {
        "version": 2,
        "widgets": widgets_dict,
        "rows": [
            {
                "id": row_id,
                "columns": [
                    {
                        "id": col_id,
                        "span": 12,
                        "widget_ids": [w.id for w in body.widgets],
                    }
                ],
            }
        ],
    }


def default_layout() -> Dict[str, Any]:
    """Default v2 layout when no row exists in DB."""
    w_date = "default-date-filter"
    w_revenue = "default-revenue"
    w_cash = "default-cash"
    w_wip = "default-wip"
    w_quotes = "default-quotes"
    w_reviews = "default-reviews"
    return {
        "version": 2,
        "widgets": {
            w_date: {"id": w_date, "type": "date_filter", "title": "Date filter"},
            w_revenue: {"id": w_revenue, "type": "revenue_snapshot", "title": "Branch revenue"},
            w_cash: {"id": w_cash, "type": "cash_in_bank", "title": "Cash balances"},
            w_wip: {"id": w_wip, "type": "wip_summary", "title": "Work in progress"},
            w_quotes: {"id": w_quotes, "type": "quotes_summary", "title": "Quotes overview"},
            w_reviews: {"id": w_reviews, "type": "google_reviews", "title": "Google reviews"},
        },
        "rows": [
            {
                "id": "default-row-1",
                "columns": [{"id": "default-col-1", "span": 12, "widget_ids": [w_date, w_revenue]}],
            },
            {
                "id": "default-row-2",
                "columns": [
                    {"id": "default-col-2a", "span": 4, "widget_ids": [w_cash]},
                    {"id": "default-col-2b", "span": 4, "widget_ids": [w_wip]},
                    {"id": "default-col-2c", "span": 4, "widget_ids": [w_quotes]},
                ],
            },
            {
                "id": "default-row-3",
                "columns": [{"id": "default-col-3", "span": 12, "widget_ids": [w_reviews]}],
            },
        ],
    }


def empty_layout_v2() -> Dict[str, Any]:
    return {"version": 2, "widgets": {}, "rows": []}


def normalize_layout_input(data: Dict[str, Any]) -> Dict[str, Any]:
    """Accept v1 or v2 payload; return validated v2 dict."""
    if not isinstance(data, dict):
        raise ValueError("Layout body must be a JSON object")
    ver = data.get("version", 1)
    if ver == 2:
        v2 = LayoutBodyV2.model_validate(data)
        return v2.model_dump()
    if ver == 1:
        v1 = LayoutBodyV1.model_validate(data)
        return migrate_v1_to_v2(v1)
    raise ValueError(f"Unsupported layout version: {ver}")


def _parse_stored_layout(raw: str) -> Dict[str, Any]:
    try:
        data = json.loads(raw)
    except json.JSONDecodeError as e:
        logger.warning("Invalid layout JSON in DB: %s", e)
        return default_layout()
    if not isinstance(data, dict):
        return default_layout()
    try:
        ver = data.get("version", 1)
        if ver == 2:
            return LayoutBodyV2.model_validate(data).model_dump()
        if ver == 1:
            v1 = LayoutBodyV1.model_validate({"version": 1, "widgets": data.get("widgets", [])})
            return migrate_v1_to_v2(v1)
        return default_layout()
    except Exception as e:
        logger.warning("Layout failed validation: %s", e)
        return default_layout()


class CustomPageCreateBody(BaseModel):
    slug: str = Field(..., min_length=1, max_length=64)
    title: Optional[str] = Field(None, max_length=200)
    view_permission_key: Optional[str] = Field(
        None,
        max_length=80,
        description="If set, only users with this permission can open /page/{slug}. Omit or null = any logged-in user.",
    )


class CustomPagePatchBody(BaseModel):
    title: Optional[str] = Field(None, max_length=200)
    new_slug: Optional[str] = Field(None, min_length=1, max_length=64)
    view_permission_key: Optional[str] = Field(
        None,
        max_length=80,
        description="Set to a valid permission key, or null to allow any logged-in user.",
    )


# --- Legacy single-page API (default slug) ---


@router.get("/layout")
async def get_custom_page_layout_legacy(
    user: User = Depends(require_permission("admin.custom_pages")),
    db: Session = Depends(get_db),
):
    row = db.query(CustomPageLayout).filter(CustomPageLayout.slug == LAYOUT_SLUG_DEFAULT).first()
    if not row:
        return default_layout()
    return _parse_stored_layout(row.layout_json)


@router.put("/layout")
async def put_custom_page_layout_legacy(
    body: Dict[str, Any],
    user: User = Depends(require_permission("admin.custom_pages")),
    db: Session = Depends(get_db),
):
    normalized = normalize_layout_input(body)
    payload = json.dumps(normalized, separators=(",", ":"))
    row = db.query(CustomPageLayout).filter(CustomPageLayout.slug == LAYOUT_SLUG_DEFAULT).first()
    now = datetime.utcnow()
    if row:
        row.layout_json = payload
        row.updated_at = now
    else:
        row = CustomPageLayout(
            slug=LAYOUT_SLUG_DEFAULT,
            title=None,
            view_permission_key=None,
            layout_json=payload,
            updated_at=now,
        )
        db.add(row)
    db.commit()
    return {"ok": True, "updated_at": row.updated_at.isoformat() + "Z", "slug": LAYOUT_SLUG_DEFAULT}


# --- Multi-page CRUD ---


@router.get("/permission-options")
async def list_custom_page_permission_options(
    user: User = Depends(require_permission("admin.custom_pages")),
    db: Session = Depends(get_db),
):
    """All permission keys (with labels) for gating who can view a custom page at /page/{slug}."""
    perms = db.query(Permission).order_by(Permission.category, Permission.key).all()
    return [
        {"key": p.key, "name": p.name, "category": p.category}
        for p in perms
    ]


@router.get("")
async def list_custom_pages(
    user: User = Depends(require_permission("admin.custom_pages")),
    db: Session = Depends(get_db),
):
    rows = db.query(CustomPageLayout).order_by(CustomPageLayout.updated_at.desc()).all()
    return [
        {
            "slug": r.slug,
            "title": r.title,
            "view_permission_key": r.view_permission_key,
            "updated_at": r.updated_at.isoformat() + "Z" if r.updated_at else None,
        }
        for r in rows
    ]


@router.post("")
async def create_custom_page(
    body: CustomPageCreateBody,
    user: User = Depends(require_permission("admin.custom_pages")),
    db: Session = Depends(get_db),
):
    slug = normalize_slug(body.slug)
    existing = db.query(CustomPageLayout).filter(CustomPageLayout.slug == slug).first()
    if existing:
        raise HTTPException(status_code=409, detail="A page with this slug already exists")
    now = datetime.utcnow()
    layout_str = json.dumps(empty_layout_v2(), separators=(",", ":"))
    vpk = _normalize_view_permission_key(db, body.view_permission_key)
    row = CustomPageLayout(
        slug=slug,
        title=body.title,
        view_permission_key=vpk,
        layout_json=layout_str,
        updated_at=now,
    )
    db.add(row)
    db.commit()
    db.refresh(row)
    return {
        "slug": row.slug,
        "title": row.title,
        "view_permission_key": row.view_permission_key,
        "updated_at": row.updated_at.isoformat() + "Z",
    }


@router.patch("/{slug}")
async def patch_custom_page(
    slug: str,
    body: CustomPagePatchBody,
    user: User = Depends(require_permission("admin.custom_pages")),
    db: Session = Depends(get_db),
):
    key = normalize_slug(slug)
    row = db.query(CustomPageLayout).filter(CustomPageLayout.slug == key).first()
    if not row:
        raise HTTPException(status_code=404, detail="Page not found")
    patch = body.model_dump(exclude_unset=True)
    if "title" in patch:
        t = patch["title"]
        row.title = None if t is None else (str(t).strip() or None)
    if "new_slug" in patch and patch["new_slug"] is not None:
        new_key = normalize_slug(patch["new_slug"])
        if new_key != key:
            clash = db.query(CustomPageLayout).filter(CustomPageLayout.slug == new_key).first()
            if clash:
                raise HTTPException(status_code=409, detail="That slug is already in use")
            row.slug = new_key
            key = new_key
    if "view_permission_key" in patch:
        row.view_permission_key = _normalize_view_permission_key(db, patch["view_permission_key"])
    row.updated_at = datetime.utcnow()
    db.commit()
    db.refresh(row)
    return {
        "slug": row.slug,
        "title": row.title,
        "view_permission_key": row.view_permission_key,
        "updated_at": row.updated_at.isoformat() + "Z",
    }


@router.delete("/{slug}")
async def delete_custom_page(
    slug: str,
    user: User = Depends(require_permission("admin.custom_pages")),
    db: Session = Depends(get_db),
):
    key = normalize_slug(slug)
    row = db.query(CustomPageLayout).filter(CustomPageLayout.slug == key).first()
    if not row:
        raise HTTPException(status_code=404, detail="Page not found")
    db.delete(row)
    db.commit()
    return {"ok": True}


@router.get("/{slug}/layout")
async def get_custom_page_layout_by_slug(
    slug: str,
    user: User = Depends(require_permission("admin.custom_pages")),
    db: Session = Depends(get_db),
):
    key = normalize_slug(slug)
    row = db.query(CustomPageLayout).filter(CustomPageLayout.slug == key).first()
    if not row:
        raise HTTPException(status_code=404, detail="Page not found")
    return _parse_stored_layout(row.layout_json)


@router.put("/{slug}/layout")
async def put_custom_page_layout_by_slug(
    slug: str,
    body: Dict[str, Any],
    user: User = Depends(require_permission("admin.custom_pages")),
    db: Session = Depends(get_db),
):
    key = normalize_slug(slug)
    row = db.query(CustomPageLayout).filter(CustomPageLayout.slug == key).first()
    if not row:
        raise HTTPException(status_code=404, detail="Page not found")
    try:
        normalized = normalize_layout_input(body)
    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e)) from e
    payload = json.dumps(normalized, separators=(",", ":"))
    now = datetime.utcnow()
    row.layout_json = payload
    row.updated_at = now
    db.commit()
    return {"ok": True, "updated_at": row.updated_at.isoformat() + "Z", "slug": row.slug}
