"""Local website editing, publication and crawler metadata. No outbound fetches."""
import json
import re
from html import escape
from pathlib import Path
from fastapi import HTTPException, Request
from fastapi.responses import HTMLResponse, Response, FileResponse
from .site_models import normalise, public_content, asset_ids, broken_links, SiteEdit
from .models import Version, Restore
from .store import one, many, audit, now
from .photos import project_paths,public_projects
from .feedback import public_reviews


def fail(message,status=409):raise HTTPException(status,message)


def setup(db):
    # SQLite transaction is shared with Store initialisation. Preserve revisions,
    # timestamps and financial rows; only the website JSON contract changes.
    row=one(db,'SELECT * FROM site WHERE id=1')
    if not row:return
    changes={key:normalise(json.loads(row[key])) for key in ('draft','published')}
    history=[(r['id'],normalise(json.loads(r['content']))) for r in many(db,'SELECT id,content FROM site_history')]
    old_version=json.loads(row['draft']).get('content_version')
    db.execute('UPDATE site SET draft=?,published=? WHERE id=1',(json.dumps(changes['draft']),json.dumps(changes['published'])))
    for rid,content in history:db.execute('UPDATE site_history SET content=? WHERE id=?',(json.dumps(content),rid))
    if old_version!=2:audit(db,'site.structured_upgrade','v0.6')


def identity(db):
    content=normalise(json.loads(one(db,'SELECT published FROM site')['published']))
    return {k:content[k] for k in ('business_name','email','phone','colour','accent_colour','logo_id','address','document_settings')}


def page_for(content,path):
    if path=='':return next((p for p in content['pages'] if p['slug']==''),None)
    if path.startswith('services/'):
        slug=path.removeprefix('services/')
        return next((s for s in content['services'] if s['slug']==slug),None)
    return next((p for p in content['pages'] if p['slug']==path),None)


def origin(request):
    # Boundary middleware validates this loopback host. Ignore forwarded values.
    return 'http://'+request.headers['host']


def head_html(content,page,base,path):
    meta=page['meta'];fallback=content['meta']
    title=meta['title'] or ((fallback['title'] or content['business_name']) if not path else page['title']+' · '+content['business_name'])
    description=meta['description'] or fallback['description'] or (page.get('description') or content['intro'])
    url=base+('/'+path if path else '')
    image=meta['image_id'] or fallback['image_id'] or content['logo_id']
    tags=[('<title>'+escape(title)+'</title>'),'<meta name="robots" content="noindex, nofollow">']
    fields=[('name','description',description),('property','og:title',title),('property','og:description',description),('property','og:type','website'),('property','og:url',url),('property','og:site_name',content['business_name']),('name','twitter:card','summary_large_image' if image else 'summary')]
    if image:fields.append(('property','og:image',base.replace('/site/','/api/public/')+'/assets/'+image))
    for attribute,key,value in fields:tags.append('<meta '+attribute+'="'+key+'" content="'+escape(value,quote=True)+'">')
    tags.append('<link rel="canonical" href="'+escape(url,quote=True)+'">')
    return '\n'.join(tags)


def mount_websites(app,store,session,once,business,static_dir):
    def current(slug):
        b=business(slug)
        with store.tenant(b['id']) as db:
            row=one(db,'SELECT published,published_at FROM site')
            return public_content(json.loads(row['published'])),row['published_at']

    def check_assets(db,content):
        owned={r['id'] for r in many(db,'SELECT id FROM assets')}
        if not asset_ids(content)<=owned:fail('An image does not belong to this business.',404)

    @app.get('/api/site')
    def read_site(request:Request):
        s=session(request)
        with store.tenant(s['business_id']) as db:
            row=one(db,'SELECT * FROM site')
            for key in ('draft','published'):row[key]=normalise(json.loads(row[key]))
            row['history']=many(db,'SELECT id,created_at FROM site_history ORDER BY id DESC')
            row['publication_errors']=broken_links(row['draft'],project_paths(db)+['reviews/selected'])
            row['audit']=many(db,"SELECT action,subject,created_at FROM audit WHERE action LIKE 'site.%' ORDER BY id DESC LIMIT 60")
            return row

    @app.put('/api/site')
    def save_site(payload:SiteEdit,request:Request):
        s=session(request,csrf=True);content=payload.content.model_dump()
        with store.tenant(s['business_id']) as db:
            def save():
                check_assets(db,content)
                if not db.execute('UPDATE site SET draft=?,version=version+1 WHERE id=1 AND version=?',(json.dumps(content),payload.version)).rowcount:
                    fail('The website changed in another window. Keep your edits, or reload the current saved draft before continuing.')
                audit(db,'site.draft_saved',s['subject']+':'+str(payload.version+1))
                return {'version':payload.version+1}
            return once(db,request,payload.model_dump(),save)

    @app.post('/api/site/publish')
    def publish(payload:Version,request:Request):
        s=session(request,csrf=True)
        with store.tenant(s['business_id']) as db:
            def action():
                row=one(db,'SELECT * FROM site')
                if row['version']!=payload.version:fail('Save or reload the current draft before publishing.')
                content=normalise(json.loads(row['draft']));check_assets(db,content)
                if broken_links(content,project_paths(db)+['reviews/selected']):fail('Fix website buttons that link to missing or unpublished pages before publishing: '+', '.join(broken_links(content,project_paths(db)+['reviews/selected'])),422)
                stamp=now();raw=json.dumps(content)
                db.execute('UPDATE site SET published=?,published_at=?,version=version+1 WHERE id=1',(raw,stamp))
                cursor=db.execute('INSERT INTO site_history(content,created_at) VALUES(?,?)',(raw,stamp))
                audit(db,'site.published_locally',s['subject']+':'+str(cursor.lastrowid))
                return {'version':payload.version+1,'revision_id':cursor.lastrowid}
            return once(db,request,payload.model_dump(),action)

    @app.post('/api/site/restore')
    def restore(payload:Restore,request:Request):
        s=session(request,csrf=True)
        with store.tenant(s['business_id']) as db:
            def action():
                row=one(db,'SELECT content FROM site_history WHERE id=?',(payload.revision_id,))
                if not row:fail('Publication not found in this business.',404)
                content=normalise(json.loads(row['content']));check_assets(db,content)
                if not db.execute('UPDATE site SET draft=?,version=version+1 WHERE id=1 AND version=?',(json.dumps(content),payload.version)).rowcount:fail('The website changed. Reload before restoring.')
                audit(db,'site.restored_to_draft',s['subject']+':'+str(payload.revision_id))
                return {'version':payload.version+1}
            return once(db,request,payload.model_dump(),action)

    @app.get('/api/public/{slug}')
    def public_site(slug:str):return current(slug)[0]

    @app.get('/api/public/{slug}/pages/{path:path}')
    def public_page(slug:str,path:str):
        content,_=current(slug);page=page_for(content,path)
        if not page:fail('This page is unavailable.',404)
        return page

    @app.get('/api/public/{slug}/assets/{asset_id}')
    def public_asset(slug:str,asset_id:str):
        content,_=current(slug)
        if asset_id not in asset_ids(content):fail('Image is not published on this website.',404)
        return FileResponse(store.asset_path(business(slug)['id'],asset_id),media_type='image/png')

    @app.get('/robots.txt')
    def robots():return Response('# Local demonstration. Search indexing is disabled.\nUser-agent: *\nDisallow: /\n',media_type='text/plain')

    @app.get('/site/{slug}/robots.txt')
    def site_robots(slug:str):
        business(slug)
        return robots()

    @app.get('/site/{slug}/sitemap.xml')
    def sitemap(slug:str,request:Request):
        content,stamp=current(slug);base=origin(request)+'/site/'+slug
        paths=[p['slug'] for p in content['pages'] if not p['meta']['noindex']]+['services/'+s['slug'] for s in content['services'] if not s['meta']['noindex']]
        with store.tenant(business(slug)['id']) as db:
            if public_reviews(db)['count']:paths+=['reviews/selected']
            paths+=['work/projects']+['work/projects/'+p['slug'] for p in public_projects(db) if not p['noindex']]
        if content['meta']['noindex']:paths=[]
        entries=['<url><loc>'+escape(base+('/'+p if p else ''))+'</loc><lastmod>'+escape(stamp[:10])+'</lastmod></url>' for p in paths]
        return Response('<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'+''.join(entries)+'</urlset>',media_type='application/xml')

    if static_dir:
        @app.get('/site/{slug}')
        @app.get('/site/{slug}/{path:path}')
        def website_shell(slug:str,request:Request,path:str=''):
            content,_=current(slug);page=page_for(content,path)
            status=200 if page else 404
            # Keep a real 404, but load the site's normal missing-page view.
            # Never use the private draft to supply a hidden page's metadata.
            if not page:page={'title':'Page not found','meta':{'title':'Page not found · '+content['business_name'],'description':'This page is unavailable. Return to the business website.','image_id':None}}
            html=(Path(static_dir)/'index.html').read_text()
            html=re.sub(r'<title>.*?</title>','',html,flags=re.S)
            html=re.sub(r'<meta\s+name="(?:description|robots)"[^>]*>','',html)
            html=html.replace('</head>',head_html(content,page,origin(request)+'/site/'+slug,path)+'</head>')
            return HTMLResponse(html,status_code=status)
