"""Recreate reviewed production content as inert semantic HTML and preserve asset URLs.
Requires lxml. Input: work/production-audit/inventory.json. No production mutations.
"""
from pathlib import Path
from urllib.parse import urljoin, urlsplit, unquote
from urllib.request import Request, urlopen
from concurrent.futures import ThreadPoolExecutor
from lxml import html, etree
import json, csv, re, hashlib, time, sys

ROOT=Path(__file__).resolve().parents[1]
WORK=ROOT/'work/production-audit'
source=json.loads((WORK/'inventory.json').read_text())
ORIGIN=source['origin']
PRESERVED={'/','/sobre','/contato','/produtos','/produtos/fios-e-cabos'}
ALLOWED={'h2','h3','h4','p','ul','ol','li','table','thead','tbody','tr','th','td','blockquote','figure','figcaption','strong','b','em','i','br','a','img','span','div','section'}
assets={}

def text(node):return ' '.join(node.text_content().split())
def safe_href(value,base):
    if value.startswith('#'): return value
    url=urlsplit(urljoin(base,value))
    if url.scheme not in ('http','https','mailto','tel'):return ''
    if url.hostname in ('portaldistribuidora.com.br','www.portaldistribuidora.com.br'):
        return url.path+('?' + url.query if url.query else '')+('#'+url.fragment if url.fragment else '')
    return url.geturl()

def sanitize(node,base):
    if not isinstance(node.tag,str):return ''
    tag=node.tag.lower()
    if tag == 'a' and re.search(r'(?:hit-?botu|siralama-yukseltme-botu|trafik-botu|organik-hit-botu)',node.get('href',''),re.I):return ''
    if tag in ('script','style','noscript','iframe','object','embed','svg','form','input','textarea','select','button','nav','header','footer','head'):return ''
    out=etree.Element(tag if tag in ALLOWED else 'div')
    out.text=node.text
    if re.fullmatch(r"[a-zA-Z][a-zA-Z0-9_:-]{0,100}",node.get("id","")):out.set("id",node.get("id"))
    if tag in ['h1','h2','h3','h4']:
        out.tag='h2' if tag=='h1' else tag
        if node.get('id'):out.set('id',node.get('id'))
    if tag=='a':
        href=safe_href(node.get('href',''),base)
        if href:out.set('href',href)
        if href.startswith(('https://wa.me/','https://api.whatsapp.com/','https://web.whatsapp.com/')):
            out.set('data-cta','legado_'+hashlib.sha256((base+etree.tostring(node,encoding='unicode')).encode()).hexdigest()[:12]);out.set('target','_blank');out.set('rel','noopener noreferrer');out.set('class','button button--primary')
        elif href.startswith('https://'):out.set('rel','noopener noreferrer')
    if tag=='img':
        src=urljoin(base,node.get('src',''))
        url=urlsplit(src)
        if url.hostname not in ('portaldistribuidora.com.br','www.portaldistribuidora.com.br') or not url.path.startswith('/wp-content/uploads/') or not re.search(r'\.(png|jpe?g|webp|gif|avif)$',url.path,re.I):return ''
        local=url.path
        if '..' in unquote(local).split('/'):return ''
        assets[local]=src
        out.set('src',local);out.set('alt',node.get('alt',''));out.set('loading','lazy');out.set('decoding','async')
        for attr in ['width','height']:
            if (node.get(attr) or '').isdigit():out.set(attr,node.get(attr))
    if tag in ('td','th'):
        for attr in ['colspan','rowspan']:
            if (node.get(attr) or '').isdigit():out.set(attr,node.get(attr))
    for child in node:
        rendered=sanitize(child,base)
        if rendered:
            element=html.fragment_fromstring(rendered)
            element.tail=child.tail
            out.append(element)
        elif child.tail:
            if len(out):out[-1].tail=(out[-1].tail or '')+child.tail
            else:out.text=(out.text or '')+child.tail
    if not ''.join(out.itertext()).strip() and not out.xpath('.//img') and out.tag not in ('img','br'):return ''
    return etree.tostring(out,method='html',encoding='unicode')

def extract_testimonials(main,base):
    """Elementor stores testimonial copy in divs, outside the paragraph importer."""
    testimonials=[]
    widgets=main.xpath('.//*[contains(concat(" ",normalize-space(@class)," ")," elementor-widget-testimonial ")]')
    for widget in widgets:
        quotes=widget.xpath('.//*[contains(concat(" ",normalize-space(@class)," ")," elementor-testimonial-content ")]')
        names=widget.xpath('.//*[contains(concat(" ",normalize-space(@class)," ")," elementor-testimonial-name ")]')
        if not quotes or not names or not text(quotes[0]) or not text(names[0]):
            raise ValueError('Incomplete testimonial in '+base)
        siblings=widget.getparent()
        stars=siblings.xpath('./*[contains(concat(" ",normalize-space(@class)," ")," elementor-widget-star-rating ")]')
        ratings=stars[0].xpath('.//*[@itemprop="ratingValue"]') if stars else []
        match=re.search(r'\b([1-5])\s+de\s+5\b',text(ratings[0])) if ratings else None
        images=widget.xpath('.//img')
        photo=sanitize(images[0],base) if images else ''
        testimonials.append({
            'name':text(names[0]),
            'quote':text(quotes[0]),
            'rating':int(match.group(1)) if match else None,
            'image':html.fragment_fromstring(photo).get('src') if photo else None,
        })
        widget.drop_tree()
        for star in stars:star.drop_tree()
    if testimonials:
        for heading in main.xpath('.//h2|.//h3'):
            if text(heading).casefold()=='o que dizem da portal distribuidora':heading.drop_tree()
    return testimonials

pages=[]
for entry in source['pages']:
    path=urlsplit(entry['url']).path.rstrip('/') or '/'
    if entry.get('asset'):
        (ROOT/'public'/path.lstrip('/')).write_text(entry['contentText']);continue
    if entry.get('blocked') or 'One moment' in entry.get('title',''):raise RuntimeError('Source requires retry: '+path)
    if path=='/wpwbot-mobile-app':continue
    raw=html.fromstring((WORK/entry['sourceFile']).read_bytes())
    candidates=raw.xpath('//*[@data-elementor-type="wp-page"] | //*[@data-elementor-type="wp-post"] | //*[@data-elementor-type="single-post"] | //*[@data-elementor-type="single"] | //main | //article')
    main=max(candidates,key=lambda x:len(x.text_content())) if candidates else (raw.find('body') if raw.find('body') is not None else raw)
    for node in main.xpath('.//script|.//style|.//noscript|.//header|.//footer|.//nav|.//form|.//*[@id="comments"]|.//*[contains(@class,"elementor-widget-posts")]|.//*[contains(@class,"ast-breadcrumbs")]'):
        if node.getparent() is not None:node.drop_tree()
    testimonials=extract_testimonials(main,entry['url'])
    h1=(entry.get('h1') or [''])[0]
    title=entry.get('title','')
    label=h1 or title.split(' - Portal Distribuidora')[0].split(' | ')[0] or path.split('/')[-1].replace('-',' ').capitalize()
    if path.startswith('/regioes-atendidas/cidades/') and path!='/regioes-atendidas/cidades':
        # Existing template uses H2 for its primary heading.
        candidates=main.xpath('.//h2')
        if candidates:label=text(candidates[0])
    blocks=[]
    # Rebuild semantic blocks, omitting Elementor wrappers, duplicate H1 and menus.
    for node in main.xpath('.//h1|.//h2|.//h3|.//h4|.//p|.//ul|.//ol|.//table|.//blockquote|.//figure|.//img|.//a[contains(@class,"elementor-button")]'):
        if any(parent.tag in ['p','ul','ol','table','blockquote','figure','h1','h2','h3','h4'] for parent in node.iterancestors() if parent is not main):continue
        if node.tag=='h1' or (node.tag in ('h2','h3') and text(node)==label):continue
        if text(node) in ('Ir para o conteúdo','Rolar para cima'):continue
        if re.search(r'hit[ -]?botu|trafik[ -]botu|siralama-yukseltme-botu',text(node),re.I):continue
        rendered=sanitize(node,entry['url'])
        if rendered and rendered not in blocks:blocks.append(rendered)
    body='\n'.join(blocks)
    plain=text(html.fragment_fromstring(body,create_parent='div')) if body else ''
    is_thin=len(plain)<180
    kind='article' if entry['sitemap'].endswith('post-sitemap.xml') and path!='/blog' else 'region' if path.startswith('/regioes-atendidas') else 'product' if path.startswith('/produtos/') else 'page'
    description=entry.get('description') or f'{label}. Consulte informações, referências e atendimento da Portal Distribuidora de materiais elétricos em Goiânia.'
    if is_thin:
        overrides={
            '/lp':'Materiais elétricos para sua obra em Goiânia',
            '/blog':'Blog de materiais elétricos e iluminação',
            '/centro-oeste':'Materiais elétricos para o Centro-Oeste',
            '/produtos/duchas-chuveiros/aco-inoxidavel':'Duchas e chuveiros de aço inoxidável',
            '/produtos/caixas-de-passagem/sobrepostas':'Caixas de passagem sobrepostas',
            '/produtos/caixas-de-passagem/embutida':'Caixas de passagem embutidas',
            '/produtos/disjuntores/diferencial-residual':'Dispositivos diferenciais residuais',
            '/produtos/fios-e-cabos/bitolas':'Bitolas de fios e cabos elétricos',
            '/produtos/moderna':'Iluminação moderna',
            '/produtos/canaletas':'Canaletas para instalações elétricas',
            '/produtos/mangueira-eletrica':'Mangueiras para instalações elétricas',
        }
        label=overrides.get(path,label)
        if path.startswith('/produtos/arandelas/'):label='Arandelas '+label.lower()
        if path.startswith('/produtos/disjuntores/') and 'diferencial' not in path:label='Disjuntores '+label.lower()
        if path.startswith('/produtos/luminarias/'):
            shapes={'embutir':'de embutir','sobrepor':'de sobrepor','retangular':'retangulares','redonda':'redondas','quadrada':'quadradas','linear':'lineares'}
            label='Luminárias '+shapes[path.split('/')[-1]]
        if path.startswith('/produtos/ferramentas/') and path.split('/')[-1] in ['medicao','teste-e-diagnostico','corte-crimpagem']:label='Ferramentas de '+label.lower()
        elif path.startswith('/produtos/ferramentas/') and path.split('/')[-1] in ['manuais','especiais']:label='Ferramentas '+label.lower()
        title=f'{label} | Portal Distribuidora em Goiânia'
        description=f'Consulte {label.lower()} com a Portal Distribuidora em Goiânia. Compare referências e organize sua cotação pelo WhatsApp.'
    if path=='/produtos/arandelas':body=body.replace('Procurando disjuntores e dispositivo de proteção?', 'Procurando arandelas e balizadores?')
    review=[]
    if is_thin:review.append('conteudo_de_origem_insuficiente')
    if kind=='article':review.append('revisar_atualidade_editorial_e_tecnica')
    if kind=='region':review.append('confirmar_cobertura_e_alegacoes_de_lojas_locais')
    if re.search('24 Horas|Melhor Preço|até 85%|Entrega.*Rápida',plain,re.I):review.append('confirmar_promessas_comerciais_da_origem')
    page={'path':path,'sourceUrl':entry['url'],'title':title,'heading':label,'description':description,'kind':kind,'html':body,'sourceCharacters':len(plain),'lastmod':entry['lastmod'],'review':review,'existingRoute':path in PRESERVED,'status':entry['status']}
    if testimonials:page['testimonials']=testimonials
    pages.append(page)

def download(item):
    path,url=item
    target=ROOT/'public'/unquote(path).lstrip('/')
    # URL-encoded filenames resolve to decoded filenames in the asset server.
    if target.exists():return {'path':path,'status':'present','bytes':target.stat().st_size}
    try:
        time.sleep(.15)
        with urlopen(Request(url,headers={'User-Agent':'PortalMigrationAssets/1.0', 'Referer':ORIGIN+'/'}),timeout=30) as response:
            data=response.read(8_000_001)
            if not response.headers.get('Content-Type','').startswith('image/') or len(data)>8_000_000:raise ValueError('unexpected content or size')
        target.parent.mkdir(parents=True,exist_ok=True);target.write_bytes(data)
        return {'path':path,'status':'copied','bytes':len(data)}
    except Exception as e:return {'path':path,'status':'failed','error':str(e)}

with ThreadPoolExecutor(max_workers=2) as pool:
    asset_results=[]
    for result in pool.map(download, [] if '--no-assets' in sys.argv else assets.items()):
        asset_results.append(result)
        if len(asset_results)%40==0:print('Assets',len(asset_results),'/',len(assets),flush=True)
(ROOT/'data/legacy-pages.json').write_text(json.dumps({'origin':ORIGIN,'auditedAt':source['auditedAt'],'pages':pages},ensure_ascii=False,indent=2))
if '--no-assets' in sys.argv:
    asset_results=[{'path':p,'sourceUrl':url,'status':'present' if (ROOT/'public'/unquote(p).lstrip('/')).exists() else 'pending'} for p,url in assets.items()]
(ROOT/'docs/production-assets.json').write_text(json.dumps(asset_results,ensure_ascii=False,indent=2))
with (ROOT/'docs/production-url-map.csv').open('w') as f:
    writer=csv.writer(f);writer.writerow(['source_url','local_path','action','source_characters','review'])
    for p in pages:writer.writerow([p['sourceUrl'],p['path'],'incorporate_content' if p['existingRoute'] else 'recreate_same_path',p['sourceCharacters'],'; '.join(p['review'])])
    writer.writerow([ORIGIN+'/locations.kml','/locations.kml','preserve_asset','',''])
    writer.writerow([ORIGIN+'/wpwbot-mobile-app/','/wpwbot-mobile-app','retire_plugin_endpoint_410','','not_a_landing_page'])
print(json.dumps({'pages':len(pages),'thin':sum(p['sourceCharacters']<180 for p in pages),'assets':len(assets),'pendingAssets':sum(a['status'] in ('failed','pending') for a in asset_results)},ensure_ascii=False))
