"""The public website — pages anyone can open without signing in (see
app/routers/public_site.py for how they are served)."""

import json
import re
import uuid
from datetime import datetime
from typing import Literal

from pydantic import BaseModel, Field, field_validator

SLUG_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
_HOST_RE = re.compile(r"^(?=.{1,253}$)(?!-)[a-z0-9-]{1,63}(?:\.(?!-)[a-z0-9-]{1,63})*(?::\d{1,5})?$")
_TOKEN_RE = re.compile(r"^[A-Za-z0-9_\-=+/.]{1,200}$")

# Blocks a public page can hold. Everything here shows fixed content or
# navigates; anything that reads or writes workspace data (lists, forms,
# charts, approvals...) or runs a task needs a signed-in person, so a page
# holding one can't be put on the public site.
PUBLIC_BLOCK_TYPES = frozenset({
    "text", "rich_text", "image", "video", "icon", "button", "link", "html", "table",
    "divider", "spacer", "page_header", "alert", "stepper",
    "section", "row", "column", "panel", "grid", "layout", "tabs", "accordion", "popup",
})
# Actions a public page's buttons and links can run.
PUBLIC_ACTION_TYPES = frozenset({"navigate", "open_external_url", "open_popup", "print"})


def _clean_url(value: str | None) -> str | None:
    value = (value or "").strip()
    if not value:
        return None
    if not (value.startswith(("https://", "http://", "/")) and not value.startswith("//")):
        raise ValueError("Use a full web address (https://…) or a path starting with /.")
    return value[:1000]


def check_slug(value: str | None, *, max_length: int) -> str | None:
    value = (value or "").strip().lower()
    if not value:
        return None
    if len(value) > max_length or not SLUG_RE.match(value):
        raise ValueError(f"Use lowercase letters, numbers and single hyphens (up to {max_length} characters), e.g. about-us.")
    return value


class PageSeo(BaseModel):
    """How a page appears in search results and when shared. Every field is
    optional: a blank title falls back to the page name, a blank description
    to the start of the page's own text."""

    title: str | None = Field(default=None, max_length=200)
    description: str | None = Field(default=None, max_length=500)
    keywords: str | None = Field(default=None, max_length=500)
    # Where search engines should treat this page as living (another address
    # showing the same content). Blank = the page's own address.
    canonical_url: str | None = None
    robots_index: bool = True
    robots_follow: bool = True
    og_title: str | None = Field(default=None, max_length=200)
    og_description: str | None = Field(default=None, max_length=500)
    og_image: str | None = None
    og_type: Literal["website", "article"] = "website"
    twitter_card: Literal["summary", "summary_large_image"] = "summary_large_image"
    # schema.org structured data — a JSON object or list, written into the
    # page as <script type="application/ld+json">.
    json_ld: str | None = Field(default=None, max_length=20000)
    # The page's language (e.g. "en", "hi"); blank = the site's.
    language: str | None = Field(default=None, max_length=20)
    include_in_sitemap: bool = True
    sitemap_priority: float | None = Field(default=None, ge=0, le=1)
    change_frequency: Literal["always", "hourly", "daily", "weekly", "monthly", "yearly", "never"] | None = None

    @field_validator("canonical_url", "og_image")
    @classmethod
    def _url(cls, value: str | None) -> str | None:
        return _clean_url(value)

    @field_validator("json_ld")
    @classmethod
    def _json(cls, value: str | None) -> str | None:
        value = (value or "").strip()
        if not value:
            return None
        try:
            parsed = json.loads(value)
        except ValueError as exc:
            raise ValueError(f"Structured data isn't valid JSON: {exc}") from exc
        if not isinstance(parsed, (dict, list)):
            raise ValueError("Structured data must be a JSON object or a list of them.")
        return value

    @field_validator("language")
    @classmethod
    def _lang(cls, value: str | None) -> str | None:
        value = (value or "").strip()
        if value and not re.match(r"^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$", value):
            raise ValueError("Use a language code such as en, hi or en-IN.")
        return value or None


class SiteSettings(BaseModel):
    """A workspace's public website (stored in tenants.site_settings; the
    address is tenants.site_slug)."""

    enabled: bool = False
    site_name: str | None = Field(default=None, max_length=200)
    # The landing page: shown at /site/<slug>/ and, for the domains below
    # (or the platform's default site), at "/" for anyone not signed in.
    home_page_id: uuid.UUID | None = None
    # How page titles are written; {page} and {site} are filled in.
    title_template: str = Field(default="{page} | {site}", max_length=200)
    default_description: str | None = Field(default=None, max_length=500)
    default_og_image: str | None = None
    favicon_url: str | None = None
    language: str = Field(default="en", max_length=20)
    # Host names (e.g. www.example.org) whose "/" opens this site's landing page.
    domains: list[str] = Field(default_factory=list, max_length=20)
    # Off: search engines are asked not to index any page (robots.txt + noindex).
    allow_indexing: bool = True
    # Ownership-verification codes from Google Search Console and Bing Webmaster Tools.
    google_site_verification: str | None = None
    bing_site_verification: str | None = None
    # Adds WebSite/Organization structured data to the landing page.
    organization_json_ld: bool = True
    organization_logo: str | None = None

    @field_validator("default_og_image", "favicon_url", "organization_logo")
    @classmethod
    def _url(cls, value: str | None) -> str | None:
        return _clean_url(value)

    @field_validator("domains")
    @classmethod
    def _domains(cls, value: list[str]) -> list[str]:
        out: list[str] = []
        for raw in value:
            host = raw.strip().lower()
            host = re.sub(r"^https?://", "", host).split("/", 1)[0]
            if not host:
                continue
            if not _HOST_RE.match(host):
                raise ValueError(f"“{raw}” isn't a host name like www.example.org.")
            if host not in out:
                out.append(host)
        return out

    @field_validator("google_site_verification", "bing_site_verification")
    @classmethod
    def _token(cls, value: str | None) -> str | None:
        value = (value or "").strip()
        # People often paste the whole <meta> tag — keep just its content.
        match = re.search(r'content\s*=\s*["\']([^"\']+)["\']', value)
        if match:
            value = match.group(1).strip()
        if value and not _TOKEN_RE.match(value):
            raise ValueError("Paste the verification code (or the whole <meta> tag) you were given.")
        return value or None

    @field_validator("title_template")
    @classmethod
    def _template(cls, value: str) -> str:
        return value.strip() or "{page} | {site}"


class SiteSettingsRead(SiteSettings):
    slug: str | None = None
    # Where the site is: /site/<slug>/ on this app's own address.
    path: str | None = None
    home_page_name: str | None = None
    # The landing page exists, is public and has been published.
    home_page_live: bool = False


class SiteSettingsUpdate(SiteSettings):
    slug: str | None = None

    @field_validator("slug")
    @classmethod
    def _slug(cls, value: str | None) -> str | None:
        return check_slug(value, max_length=60)


class PageSiteUpdate(BaseModel):
    """PATCH /pages/{id}/site — where the page is on the public site."""

    is_public: bool
    slug: str | None = None

    @field_validator("slug")
    @classmethod
    def _slug(cls, value: str | None) -> str | None:
        return check_slug(value, max_length=120)


class PublicSiteInfo(BaseModel):
    slug: str
    name: str
    home_slug: str | None = None
    language: str = "en"


class PublicPageRead(BaseModel):
    """GET /public/sites/{slug}/pages/... — a published public page, for the
    web app to draw (blocks pages; html pages are served whole by /site/)."""

    id: uuid.UUID
    name: str
    slug: str | None
    content_mode: Literal["blocks", "html"]
    blocks: list[dict]
    canvas_width: int
    fit_mode: str
    title: str
    seo: dict
    site: PublicSiteInfo
    # page id -> its address, for buttons and links that navigate to another public page.
    links: dict[str, str] = {}
    # The workspace's brand color, font and corners (TenantTheme), so the page looks as it does signed in.
    theme: dict = {}
    updated_at: datetime | None = None


class SiteRootRead(BaseModel):
    slug: str | None = None


class PageAssetRead(BaseModel):
    id: uuid.UUID
    filename: str
    content_type: str
    size_bytes: int
    url: str


class HtmlImportedPage(BaseModel):
    id: uuid.UUID
    name: str
    slug: str | None


class HtmlImportResult(BaseModel):
    pages: list[HtmlImportedPage]
    assets: int = 0
    notes: list[str] = []
