from pydantic import BaseModel, ConfigDict, Field
from typing import Optional
from datetime import datetime
from uuid import uuid4


class CardPosition(BaseModel):
    session_id: str
    x: float = 0
    y: float = 0
    width: float = 420
    height: float = 280


class ViewCardPosition(BaseModel):
    output_id: str
    # Which instance of the app this card is (1 = primary). Persisted or Pydantic strips it on save and a reloaded second-instance card collapses onto the primary's runtime (same failure shape as the browser-card dashboard_id bleed).
    instance: int = 1
    x: float = 0
    y: float = 0
    width: float = 480
    height: float = 360
    # Chat session this app preview lives inside (renders over the chat's dock slot); None = free card.
    docked_to: Optional[str] = None


class BrowserTab(BaseModel):
    id: str
    url: str = ""
    title: str = ""
    favicon: Optional[str] = None


class BrowserCardPosition(BaseModel):
    browser_id: str
    url: str = ""
    tabs: list[BrowserTab] = Field(default_factory=list)
    activeTabId: str = ""
    x: float = 0
    y: float = 0
    width: float = 1280
    height: float = 800
    # Agent session id that spawned this browser, or None for user-created. Used by the frontend to auto-remove the browser when its owner agent reaches a terminal completed/error state.
    spawned_by: Optional[str] = None
    # When the agent leaves the deliverable on the page (a video playing, a page to read), it sets this so the frontend's auto-close on parent finish skips the card and the browser stays put.
    keep_open: bool = False
    # The dashboard this card calls home. Persisted so the home survives a save; without it the card reloads untagged and renders on EVERY dashboard (the cross-dashboard bleed).
    dashboard_id: Optional[str] = None
    # Chat session this browser lives inside (renders over the chat's dock slot); None = free card.
    docked_to: Optional[str] = None
    # When this card was spawned, so the idle-agent-card reaper can keep the newest and drop the rest. Cards saved before this field existed read as None and reap first, which is right: they are the oldest thing on the canvas.
    created_at: Optional[datetime] = None


class DashboardLayout(BaseModel):
    model_config = ConfigDict(extra="allow")
    cards: dict[str, CardPosition] = Field(default_factory=dict)
    view_cards: dict[str, ViewCardPosition] = Field(default_factory=dict)
    browser_cards: dict[str, BrowserCardPosition] = Field(default_factory=dict)
    workflow_cards: dict = Field(default_factory=dict)
    workflows_hub: Optional[dict] = None
    expanded_session_ids: list[str] = Field(default_factory=list)


class Dashboard(BaseModel):
    id: str = Field(default_factory=lambda: uuid4().hex)
    name: str = "Untitled Dashboard"
    auto_named: bool = False
    created_at: datetime = Field(default_factory=datetime.now)
    updated_at: datetime = Field(default_factory=datetime.now)
    layout: DashboardLayout = Field(default_factory=DashboardLayout)
    thumbnail: Optional[str] = None
    # Bumped only when a fresh thumbnail is saved; drives sidebar/grid order so merely opening a dashboard doesn't reshuffle the list.
    preview_updated_at: Optional[datetime] = None
    # Sorted card-id set captured with the last thumbnail; lets the client tell if cards were added/removed since.
    preview_signature: Optional[str] = None


class DashboardCreate(BaseModel):
    name: str = "Untitled Dashboard"


class DashboardUpdate(BaseModel):
    name: Optional[str] = None
    layout: Optional[DashboardLayout] = None
    thumbnail: Optional[str] = None
    preview_signature: Optional[str] = None
