"""Saved layouts: a named snapshot of one frame's widget arrangement
(types, placement, per-widget settings, button assignments) that a user
can capture and later apply to any frame they control whose grid matches
-- see models.SavedLayout and docs/widgets.md's "Saved layouts" section.

Layouts are owned by a *user*, not a frame (SavedLayout.user_id), so
every endpoint here except save/apply/list (which need a frame_id to act
on) is frame-agnostic -- /api/layouts/{id} rather than /api/frames/{fid}/
layouts/{id}, gated purely on "is this your own saved layout" rather than
frame view/control.

Save/apply DO live under /api/frames/{frame_id}/... (require_frame_
control) since they read/replace one specific frame's actual widgets.
"""

from __future__ import annotations

import time

from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel
from sqlalchemy import delete, func, select
from sqlalchemy.orm import Session

from .. import grid
from ..auth import require_frame_control, require_frame_view, require_user_api
from ..db import frame_locked, get_db
from ..models import (
    Frame,
    FrameButtonAction,
    FrameCalendar,
    FrameTaskList,
    SavedLayout,
    SavedLayoutButtonAction,
    SavedLayoutSource,
    SavedLayoutWidget,
    User,
    WIDGET_CONFIG_MODELS,
    Widget,
)

router = APIRouter()

MAX_LAYOUT_NAME_LEN = 60

# Per-widget-type allowlist of config columns that are an actual authored
# *setting* (captured/restored by a saved layout) as opposed to runtime/
# cache state (a photo widget's current queue position, a calendar's
# fetch cache, a whiteboard's rendered-image cache, ...) which a layout
# deliberately leaves out -- applying a layout should feel like a fresh
# widget of that type with these settings pre-filled, not a resurrection
# of stale queue/cache state from whenever it was saved. "static"
# excludes `image` on purpose -- that BLOB lives on SavedLayoutWidget.image
# instead (see its own model docstring).
LAYOUT_CONFIG_FIELDS: dict[str, tuple[str, ...]] = {
    "photos": ("album_id", "order", "display_mode", "queue_target_len"),
    "calendar": (
        "view", "week_start", "weather_enabled", "weather_units", "weather_cities",
        "week_days", "week_layout", "week_start_offset",
    ),
    "tasks": ("name", "show_completed"),
    "static": ("display_mode", "original_filename"),
    "text": ("content", "font_size", "font_family", "align", "background_color"),
    "whiteboard": ("user_id", "url"),
}

# widget_type -> (FrameCalendar|FrameTaskList model, SavedLayoutSource.kind)
SOURCE_MODELS: dict[str, tuple[type, str]] = {
    "calendar": (FrameCalendar, "calendar"),
    "tasks": (FrameTaskList, "task"),
}


def _layout_summary(db: Session, layout: SavedLayout, frame: Frame | None = None) -> dict:
    widget_count = db.scalar(
        select(func.count()).select_from(SavedLayoutWidget).where(SavedLayoutWidget.saved_layout_id == layout.id)
    )
    result = {
        "id": layout.id,
        "name": layout.name,
        "cols": layout.cols,
        "rows": layout.rows,
        "widget_count": widget_count or 0,
        "created_at": layout.created_at,
        "updated_at": layout.updated_at,
    }
    if frame is not None:
        cols, rows = grid.grid_dims(frame.orientation)
        result["compatible"] = (layout.cols, layout.rows) == (cols, rows)
    return result


def _user_owned_layout(db: Session, layout_id: int, user: User) -> SavedLayout:
    layout = db.get(SavedLayout, layout_id)
    if layout is None or layout.user_id != user.id:
        raise HTTPException(404, "No such saved layout")
    return layout


@router.get("/api/frames/{frame_id}/layouts")
def api_layouts_list(request: Request, frame: Frame = Depends(require_frame_view), db: Session = Depends(get_db)):
    """Every saved layout owned by the calling user (saved layouts are
    global to a user, not scoped to this or any other frame) -- includes
    a `compatible` flag per layout for whether it can actually be applied
    to *this* frame's current grid, so the UI can offer incompatible ones
    greyed-out with a reason rather than hiding them outright."""
    user = require_user_api(request, db)
    layouts = db.scalars(
        select(SavedLayout).where(SavedLayout.user_id == user.id).order_by(SavedLayout.name)
    ).all()
    return {"layouts": [_layout_summary(db, layout, frame) for layout in layouts]}


class LayoutSaveRequest(BaseModel):
    name: str


def _snapshot_widget_config(widget: Widget, config) -> dict:
    fields = LAYOUT_CONFIG_FIELDS.get(widget.widget_type, ())
    return {field: getattr(config, field) for field in fields}


def _snapshot_sources(db: Session, widget_id: int, model: type) -> list[dict]:
    rows = db.scalars(select(model).where(model.widget_id == widget_id)).all()
    return [
        {
            "user_id": row.user_id, "calendar_key": row.calendar_key, "calendar_label": row.calendar_label,
            "included": row.included, "color_index": row.color_index,
        }
        for row in rows
    ]


@router.post("/api/frames/{frame_id}/layouts")
def api_layout_save(
    body: LayoutSaveRequest, request: Request,
    frame: Frame = Depends(require_frame_control), db: Session = Depends(get_db),
):
    """Snapshots this frame's current widgets (placement + settings,
    see LAYOUT_CONFIG_FIELDS), their calendar/task sources, and their
    button-action bindings into a named layout owned by the calling
    user. Saving again with a name this user already has overwrites that
    layout's snapshot in place (see models.SavedLayout's docstring)
    rather than erroring or creating a duplicate."""
    user = require_user_api(request, db)
    name = body.name.strip()
    if not name:
        raise HTTPException(400, "Name is required")
    if len(name) > MAX_LAYOUT_NAME_LEN:
        raise HTTPException(400, f"Name must be {MAX_LAYOUT_NAME_LEN} characters or fewer")

    cols, rows = grid.grid_dims(frame.orientation)
    widgets = db.scalars(
        select(Widget).where(Widget.frame_id == frame.id).order_by(Widget.sort_order)
    ).all()

    now = time.time()
    existing = db.execute(
        select(SavedLayout).where(SavedLayout.user_id == user.id, SavedLayout.name == name)
    ).scalar_one_or_none()
    if existing is not None:
        layout = existing
        layout.cols, layout.rows, layout.updated_at = cols, rows, now
        db.execute(delete(SavedLayoutWidget).where(SavedLayoutWidget.saved_layout_id == layout.id))
        db.flush()
    else:
        layout = SavedLayout(user_id=user.id, name=name, cols=cols, rows=rows, created_at=now, updated_at=now)
        db.add(layout)
        db.flush()

    snapshot_id_by_widget_id: dict[int, int] = {}
    for widget in widgets:
        config = db.get(WIDGET_CONFIG_MODELS[widget.widget_type], widget.id)
        snapshot = SavedLayoutWidget(
            saved_layout_id=layout.id, widget_type=widget.widget_type,
            x=widget.x, y=widget.y, w=widget.w, h=widget.h, sort_order=widget.sort_order,
            config=_snapshot_widget_config(widget, config),
            image=config.image if widget.widget_type == "static" else None,
        )
        db.add(snapshot)
        db.flush()
        snapshot_id_by_widget_id[widget.id] = snapshot.id

        source_model = SOURCE_MODELS.get(widget.widget_type)
        if source_model is not None:
            model, kind = source_model
            for source in _snapshot_sources(db, widget.id, model):
                db.add(SavedLayoutSource(saved_layout_widget_id=snapshot.id, kind=kind, **source))

    actions = db.scalars(select(FrameButtonAction).where(FrameButtonAction.frame_id == frame.id)).all()
    for action in actions:
        snapshot_id = snapshot_id_by_widget_id.get(action.widget_id)
        if snapshot_id is None:
            continue
        db.add(SavedLayoutButtonAction(
            saved_layout_widget_id=snapshot_id, button=action.button, action=action.action,
            sort_order=action.sort_order,
        ))

    db.commit()
    return _layout_summary(db, layout, frame)


class LayoutRenameRequest(BaseModel):
    name: str


@router.patch("/api/layouts/{layout_id}")
def api_layout_rename(layout_id: int, body: LayoutRenameRequest, request: Request, db: Session = Depends(get_db)):
    user = require_user_api(request, db)
    layout = _user_owned_layout(db, layout_id, user)
    name = body.name.strip()
    if not name:
        raise HTTPException(400, "Name is required")
    if len(name) > MAX_LAYOUT_NAME_LEN:
        raise HTTPException(400, f"Name must be {MAX_LAYOUT_NAME_LEN} characters or fewer")
    conflict = db.execute(
        select(SavedLayout).where(
            SavedLayout.user_id == user.id, SavedLayout.name == name, SavedLayout.id != layout.id
        )
    ).scalar_one_or_none()
    if conflict is not None:
        raise HTTPException(400, "You already have a saved layout with that name")
    layout.name = name
    layout.updated_at = time.time()
    db.commit()
    return _layout_summary(db, layout)


@router.delete("/api/layouts/{layout_id}")
def api_layout_delete(layout_id: int, request: Request, db: Session = Depends(get_db)):
    user = require_user_api(request, db)
    layout = _user_owned_layout(db, layout_id, user)
    db.delete(layout)
    db.commit()
    return {"status": "deleted"}


@router.post("/api/frames/{frame_id}/layouts/{layout_id}/apply")
def api_layout_apply(
    layout_id: int, request: Request,
    frame: Frame = Depends(require_frame_control), db: Session = Depends(get_db),
):
    """Replaces this frame's entire widget arrangement with a saved
    layout's -- every current widget (and its own config/sources/button
    actions, all ondelete="CASCADE") is deleted first, same "act
    unconditionally on the server, confirm on the client" posture as
    api_widgets.api_widgets_clear. A source whose owning user account
    (or a whiteboard's user_id) no longer exists is silently dropped
    rather than left dangling -- config is JSON, not FK-checked, so
    nothing enforces that at the storage layer."""
    user = require_user_api(request, db)
    layout = _user_owned_layout(db, layout_id, user)
    cols, rows = grid.grid_dims(frame.orientation)
    if (layout.cols, layout.rows) != (cols, rows):
        raise HTTPException(400, "This layout was saved for a different frame size/orientation")

    snapshots = db.scalars(
        select(SavedLayoutWidget)
        .where(SavedLayoutWidget.saved_layout_id == layout.id)
        .order_by(SavedLayoutWidget.sort_order)
    ).all()

    with frame_locked(db, frame.id):
        for widget in db.scalars(select(Widget).where(Widget.frame_id == frame.id)).all():
            db.delete(widget)
        db.flush()

        new_widget_id_by_snapshot_id: dict[int, int] = {}
        for snapshot in snapshots:
            widget = Widget(
                frame_id=frame.id, widget_type=snapshot.widget_type,
                x=snapshot.x, y=snapshot.y, w=snapshot.w, h=snapshot.h,
                sort_order=snapshot.sort_order, created_at=time.time(),
            )
            db.add(widget)
            db.flush()
            new_widget_id_by_snapshot_id[snapshot.id] = widget.id

            config = WIDGET_CONFIG_MODELS[snapshot.widget_type](widget_id=widget.id)
            for field, value in snapshot.config.items():
                if field == "user_id" and value is not None and db.get(User, value) is None:
                    value = None  # that WebDAV account no longer exists
                setattr(config, field, value)
            if snapshot.widget_type == "static" and snapshot.image is not None:
                config.image = snapshot.image
                config.uploaded_at = time.time()
            db.add(config)

            source_model = SOURCE_MODELS.get(snapshot.widget_type)
            if source_model is not None:
                model, kind = source_model
                sources = db.scalars(
                    select(SavedLayoutSource).where(
                        SavedLayoutSource.saved_layout_widget_id == snapshot.id, SavedLayoutSource.kind == kind
                    )
                ).all()
                for source in sources:
                    if db.get(User, source.user_id) is None:
                        continue  # that account no longer exists
                    db.add(model(
                        widget_id=widget.id, user_id=source.user_id, calendar_key=source.calendar_key,
                        calendar_label=source.calendar_label, included=source.included,
                        color_index=source.color_index,
                    ))

        for snapshot in snapshots:
            actions = db.scalars(
                select(SavedLayoutButtonAction)
                .where(SavedLayoutButtonAction.saved_layout_widget_id == snapshot.id)
                .order_by(SavedLayoutButtonAction.sort_order)
            ).all()
            for action in actions:
                db.add(FrameButtonAction(
                    frame_id=frame.id, button=action.button,
                    widget_id=new_widget_id_by_snapshot_id[snapshot.id], action=action.action,
                    sort_order=action.sort_order, created_at=time.time(),
                ))
        db.commit()
    return {"status": "applied", "widget_count": len(snapshots)}
