"""Structured website contracts. Text is plain text, never executable markup."""
import copy
import re
from urllib.parse import urlsplit
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator

ASSET = r'^[a-f0-9]{32}$'
SLUG = r'^[a-z0-9]+(?:-[a-z0-9]+)*$'
RESERVED = {'api','admin','assets','workspace','portal','preview','site','documents','login','logout','home','robots','sitemap','favicon','manifest'}


class Strict(BaseModel):
    model_config = ConfigDict(extra='forbid', str_strip_whitespace=True)


class LegacyService(Strict):
    title: str = Field(min_length=2,max_length=100)
    description: str = Field(min_length=5,max_length=500)


class LegacyGallery(Strict):
    asset_id: str = Field(pattern=ASSET)
    caption: str = Field(default='',max_length=160)


class LegacySiteContent(Strict):
    business_name: str = Field(min_length=2,max_length=100)
    tagline: str = Field(min_length=3,max_length=160)
    intro: str = Field(min_length=10,max_length=1200)
    about: str = Field(min_length=10,max_length=3000)
    area: str = Field(min_length=2,max_length=150)
    email: str = Field(default='',max_length=180)
    phone: str = Field(default='',max_length=40)
    colour: str = Field(pattern=r'^#[0-9a-fA-F]{6}$')
    logo_id: str | None = Field(default=None,pattern=ASSET)
    services: list[LegacyService] = Field(min_length=1,max_length=12)
    gallery: list[LegacyGallery] = Field(default_factory=list,max_length=30)


def safe_slug(value):
    if not re.fullmatch(SLUG,value) or len(value)>64 or value in RESERVED:
        raise ValueError('Use a unique short URL with lowercase letters, numbers and single hyphens. App paths are reserved.')
    return value


def safe_link(value):
    if not value:return value
    if any(ord(c)<33 for c in value) or any(c in value for c in '\\<>"\'') or '%' in value:
        raise ValueError('Use a plain site path or an HTTPS link without encoded characters.')
    if value.startswith('/') and not value.startswith('//'):
        if not re.fullmatch(r'/(?:[a-z0-9]+(?:-[a-z0-9]+)*)(?:/[a-z0-9]+(?:-[a-z0-9]+)*)?|/',value):
            raise ValueError('Use /contact, another page path or /services/service-name.')
        if value.strip('/').split('/')[0] in RESERVED:
            raise ValueError('App routes cannot be used as website links.')
        return value
    parsed=urlsplit(value)
    if parsed.scheme!='https' or not parsed.hostname or parsed.username or parsed.password or parsed.port not in (None,443):
        raise ValueError('External links must use HTTPS without login details or a custom port.')
    if not re.fullmatch(r'[a-zA-Z0-9.-]+',parsed.hostname):raise ValueError('Use a valid HTTPS hostname.')
    return value


class Meta(Strict):
    title: str = Field(default='',max_length=120)
    description: str = Field(default='',max_length=300)
    image_id: str | None = Field(default=None,pattern=ASSET)
    noindex: bool = Field(default=False,strict=True)


class Gallery(LegacyGallery):
    alt: str = Field(default='',max_length=200)
    visible: bool = Field(default=True,strict=True)


class Answer(Strict):
    question: str = Field(min_length=1,max_length=180)
    answer: str = Field(min_length=1,max_length=2000)


class Section(Strict):
    id: str = Field(pattern=r'^[a-z][a-z0-9-]{0,63}$')
    kind: Literal['intro','story','hero','text','image','services','gallery','contact','callout','faq']
    visible: bool = Field(default=True,strict=True)
    eyebrow: str = Field(default='',max_length=100)
    heading: str = Field(default='',max_length=180)
    body: str = Field(default='',max_length=6000)
    image_id: str | None = Field(default=None,pattern=ASSET)
    alt: str = Field(default='',max_length=200)
    link_label: str = Field(default='',max_length=70)
    link: str = Field(default='',max_length=500)
    items: list[Answer] = Field(default_factory=list,max_length=20)

    @field_validator('link')
    @classmethod
    def valid_link(cls,value):return safe_link(value)

    @model_validator(mode='after')
    def check_link(self):
        if bool(self.link)!=bool(self.link_label):raise ValueError('Give each button both its text and destination.')
        return self


class Page(Strict):
    id: str = Field(pattern=r'^[a-z][a-z0-9-]{0,63}$')
    slug: str = Field(max_length=64)
    title: str = Field(min_length=1,max_length=100)
    nav_label: str = Field(min_length=1,max_length=40)
    visible: bool = Field(default=True,strict=True)
    in_navigation: bool = Field(default=True,strict=True)
    in_footer: bool = Field(default=False,strict=True)
    meta: Meta = Field(default_factory=Meta)
    sections: list[Section] = Field(default_factory=list,max_length=20)

    @model_validator(mode='after')
    def check(self):
        if self.id=='home':
            if self.slug or not self.visible:raise ValueError('The homepage must stay available at the site root.')
        else:safe_slug(self.slug)
        if self.id=='contact' and (self.slug!='contact' or not self.visible):raise ValueError('Keep the Contact page available at /contact for enquiries.')
        if self.slug in ('contact','services') and self.id!=self.slug:raise ValueError('Contact and Services routes are reserved for their original pages.')
        ids=[s.id for s in self.sections]
        if len(set(ids))!=len(ids):raise ValueError('Section IDs must be unique within a page.')
        return self


class Service(LegacyService):
    slug: str = Field(min_length=1,max_length=64)
    visible: bool = Field(default=True,strict=True)
    long_description: str = Field(default='',max_length=6000)
    image_id: str | None = Field(default=None,pattern=ASSET)
    alt: str = Field(default='',max_length=200)
    meta: Meta = Field(default_factory=Meta)
    sections: list[Section] = Field(default_factory=list,max_length=12)

    @field_validator('slug')
    @classmethod
    def valid_slug(cls,value):return safe_slug(value)

    @model_validator(mode='after')
    def unique(self):
        if len({s.id for s in self.sections})!=len(self.sections):raise ValueError('Service section IDs must be unique.')
        return self


class DocumentSettings(Strict):
    company_number: str = Field(default='',max_length=80)
    vat_number: str = Field(default='',max_length=80)
    footer: str = Field(default='',max_length=1000)
    payment_terms: str = Field(default='',max_length=2000)


class SiteContent(LegacySiteContent):
    content_version: Literal[2] = 2
    address: str = Field(default='',max_length=400)
    accent_colour: str = Field(default='#d7aa52',pattern=r'^#[0-9a-fA-F]{6}$')
    footer_text: str = Field(default='',max_length=500)
    enquiry_success_message: str = Field(default='Your test enquiry has been saved. No email has been sent.',min_length=5,max_length=600)
    document_settings: DocumentSettings = Field(default_factory=DocumentSettings)
    meta: Meta = Field(default_factory=Meta)
    services: list[Service] = Field(default_factory=list,max_length=30)
    gallery: list[Gallery] = Field(default_factory=list,max_length=60)
    pages: list[Page] = Field(min_length=2,max_length=30)

    @field_validator('email')
    @classmethod
    def valid_email(cls,value):
        if value and not re.fullmatch(r'[^\s@<>"\r\n]+@[^\s@<>"\r\n]+\.[^\s@<>"\r\n]+',value):raise ValueError('Enter a public email address or leave it empty.')
        return value

    @model_validator(mode='after')
    def coherent(self):
        if len({p.id for p in self.pages})!=len(self.pages) or len({p.slug for p in self.pages})!=len(self.pages):raise ValueError('Page names need unique IDs and URLs.')
        if not {'home','contact'}<={p.id for p in self.pages}:raise ValueError('Keep the Home and Contact pages.')
        if len({s.slug for s in self.services})!=len(self.services):raise ValueError('Each service needs a unique URL.')
        if len({g.asset_id for g in self.gallery})!=len(self.gallery):raise ValueError('An image can occur only once in the shared gallery.')
        contact=next(p for p in self.pages if p.id=='contact')
        if not any(s.visible and s.kind=='contact' for s in contact.sections):raise ValueError('Keep an enquiry section on the Contact page.')
        return self


class SiteEdit(Strict):
    version: int = Field(strict=True,ge=1)
    content: SiteContent


def normalise(raw):
    """Deterministic v0.5 -> structured content upgrade; no original SHS import."""
    if raw.get('content_version')==2:return SiteContent.model_validate(raw).model_dump()
    legacy=LegacySiteContent.model_validate(raw).model_dump()
    result=copy.deepcopy(legacy)
    used=set()
    for i,service in enumerate(result['services']):
        slug=re.sub('[^a-z0-9]+','-',service['title'].lower()).strip('-')[:55] or 'service'
        if slug in RESERVED:slug='service-'+slug
        base=slug;n=2
        while slug in used:slug=base+'-'+str(n);n+=1
        used.add(slug);service['slug']=slug
    def section(id,kind,**kwargs):return dict(id=id,kind=kind,**kwargs)
    def page(id,title,sections):return dict(id=id,slug='' if id=='home' else id,title=title,nav_label=title,sections=sections)
    result['pages']=[
        page('home','Home',[section('intro','intro',eyebrow='HOME MAINTENANCE, WITH CARE',link_label='Tell us about your job',link='/contact'),section('services','services',heading='How we can help'),section('about','callout',heading='A practical, personal approach',link_label='Meet the business',link='/about')]),
        page('services','Services',[section('services','services',heading='Our services',body='Start with the job you have in mind. We can discuss the details, timing and a written quote.')]),
        page('work','Our work',[section('gallery','gallery',heading='Our work')]),
        page('about','About',[section('about','story',heading='About '+legacy['business_name'])]),
        page('contact','Contact',[section('contact','contact',heading='Tell us about your job',body='Tell us what needs doing and we can plan the next step.')]),
    ]
    return SiteContent.model_validate(result).model_dump()


def asset_ids(content):
    ids=set()
    def walk(value):
        if isinstance(value,dict):
            for key,item in value.items():
                if key in ('asset_id','image_id','logo_id') and item:ids.add(item)
                else:walk(item)
        elif isinstance(value,list):
            for item in value:walk(item)
    walk(content)
    return ids


def public_content(raw):
    value=normalise(raw)
    value.pop('document_settings')
    value['pages']=[p for p in value['pages'] if p['visible']]
    value['services']=[s for s in value['services'] if s['visible']]
    for item in value['pages']+value['services']:
        item['sections']=[s for s in item['sections'] if s['visible']]
    sections=[s for p in value['pages']+value['services'] for s in p['sections']]
    if not any(s['kind']=='story' for s in sections):value['about']=''
    if not any(s['kind']=='intro' for s in sections):
        value['tagline']='';value['intro']=''
    # Gallery items are public only when a visible gallery section uses them.
    uses_gallery=any(s['kind']=='gallery' for p in value['pages']+value['services'] for s in p['sections'])
    value['gallery']=[g for g in value['gallery'] if g['visible']] if uses_gallery else []
    return value


def broken_links(content,extra_routes=()):
    routes={'/','/contact'}|{'/'+p['slug'] for p in content['pages'] if p['visible']}|{'/services/'+s['slug'] for s in content['services'] if s['visible']}
    routes|={'/'+p for p in extra_routes}
    problems=[]
    for page in content['pages']+content['services']:
        if not page['visible']:continue
        for section in page['sections']:
            if section['visible'] and section['link'].startswith('/') and section['link'] not in routes:
                problems.append(page['title']+': '+section['link'])
    return problems
