"""Authenticated, in-memory PDF output from saved document snapshots only.

No HTML, URL loading, arbitrary paths or public file cache. DejaVu fonts and
their redistribution notices are bundled. Current balances are a separate
section, never substituted for the original amounts.
"""
import io
import re
import threading
import unicodedata
from pathlib import Path
from xml.sax.saxutils import escape
from fastapi import HTTPException, Request
from fastapi.responses import Response
from reportlab.lib import colors
from reportlab.lib.enums import TA_RIGHT
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import ParagraphStyle
from reportlab.lib.units import mm
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.platypus import BaseDocTemplate, Frame, PageTemplate, Paragraph, Spacer, Table, TableStyle, Image, KeepTogether
from .documents import get_document
from .sales_outputs import request_snapshot
from .store import now

_lock=threading.Lock()
_ready=False
WIDTH=174*mm


def fonts():
    global _ready
    with _lock:
        if not _ready:
            folder=Path(__file__).with_name('pdf_fonts')
            for name,file in (('SHS-Regular','DejaVuSans.ttf'),('SHS-Bold','DejaVuSans-Bold.ttf')):
                pdfmetrics.registerFont(TTFont(name,str(folder/file)))
            pdfmetrics.registerFontFamily('SHS-Regular',normal='SHS-Regular',bold='SHS-Bold',italic='SHS-Regular',boldItalic='SHS-Bold')
            _ready=True


def money(pence):
    if type(pence) is not int:raise ValueError('PDF amounts must be integer pence.')
    sign='-' if pence<0 else '';whole,fraction=divmod(abs(pence),100)
    return f'{sign}£{whole:,}.{fraction:02d}'


def plain(value):
    text=str(value or '')
    cmap=pdfmetrics.getFont('SHS-Regular').face.charToGlyph
    if any((unicodedata.category(c).startswith('C') and c not in '\n\t') or (ord(c) not in cmap and c not in '\n\t') for c in text):
        raise HTTPException(422,'This document contains a character unsupported by the bundled PDF font. Review the text before exporting.')
    return escape(text).replace('\t','    ').replace('\n','<br/>')


def render_document(document,kind,logo=None,request_data=None,generated_at=None):
    fonts();stamp=generated_at or now();s=request_data['document'] if request_data else document['snapshot']
    brand=s['business'];cfg=brand.get('document_settings',{})
    # Colour came from a validated identity, but recovery and legacy data may differ.
    colour=brand.get('colour','#245648')
    accent=colors.HexColor(colour) if re.fullmatch(r'#[0-9a-fA-F]{6}',colour) else colors.HexColor('#245648')
    base=ParagraphStyle('body',fontName='SHS-Regular',fontSize=9,leading=13,spaceAfter=3,splitLongWords=True)
    small=ParagraphStyle('small',parent=base,fontSize=8,leading=11)
    bold=ParagraphStyle('bold',parent=base,fontName='SHS-Bold')
    heading=ParagraphStyle('heading',parent=bold,fontSize=12,leading=17,textColor=accent,spaceBefore=6,spaceAfter=4,keepWithNext=True)
    title=ParagraphStyle('title',parent=heading,fontSize=21 if request_data else 27,leading=27 if request_data else 32,alignment=TA_RIGHT,spaceBefore=0)
    right=ParagraphStyle('right',parent=base,alignment=TA_RIGHT)
    p=lambda value,style=base:Paragraph(plain(value),style)
    story=[]
    title_text='Payment request' if request_data else kind.title()
    left=[]
    if logo:
        # Caller resolves an exact historical, owned PNG. PIL verifies bounded dimensions at upload.
        img=Image(io.BytesIO(logo));scale=min(42*mm/img.imageWidth,16*mm/img.imageHeight)
        img.drawWidth=img.imageWidth*scale;img.drawHeight=img.imageHeight*scale;img.hAlign='LEFT';left.append(img)
    left.extend([p(brand['business_name'],bold),p('\n'.join(str(brand.get(k,'')) for k in ('address','email','phone') if brand.get(k)),small)])
    for key,label in (('company_number','Company number'),('vat_number','VAT number')):
        if cfg.get(key):left.append(p(label+': '+cfg[key],small))
    right_head=[p(title_text,title),p(document['reference'],right),p('Document date: '+(s.get('document_date') or document['created_at'][:10]),right)]
    if s.get('valid_until') and kind=='quote':right_head.append(p('Valid until: '+s['valid_until'],right))
    if s.get('due_date') and kind=='invoice':right_head.append(p('Invoice due: '+s['due_date'],right))
    header=Table([[left,right_head]],colWidths=[WIDTH*.53,WIDTH*.47],hAlign='LEFT')
    header.setStyle(TableStyle([('VALIGN',(0,0),(-1,-1),'TOP'),('LEFTPADDING',(0,0),(-1,-1),0),('RIGHTPADDING',(0,0),(-1,-1),0)]))
    story += [header,Spacer(1,5*mm),p('LOCAL DEMONSTRATION — NOT FOR REAL CUSTOMER USE',bold)]
    if document['status']=='draft':story.append(p('DRAFT — content has not been issued or shared.',bold))
    if request_data:
        t=request_data['stage']
        story += [p(t['label'],heading),p(request_data['qualification']),p('Original stage amount: '+money(t['amount_pence']),bold),
                  p('Amount requested when this snapshot was recorded: '+money(request_data['requested_remaining_pence']),bold),
                  p('Requested: '+t['requested_at']+' · Due: '+(t['due_date'] or 'Not set'),small),
                  p('Snapshot recorded: '+request_data['recorded_at']+' · '+request_data['origin'],small)]
    story += [p('Prepared for',heading),p(s['customer']['name'],bold),p('\n'.join(str(s['customer'].get(k,'')) for k in ('address','email','phone') if s['customer'].get(k))),
              p(s['service'],heading),p('Original invoice content' if request_data else 'Saved document content',small)]
    rows=[[p('Description',bold),p('Qty',bold),p('Unit price',bold),p('Amount',bold)]]
    from .models import Line
    totals=s.get('line_totals_pence') or [Line.model_validate(line).total_pence() for line in s['items']]
    for line,total in zip(s['items'],totals,strict=True):
        rows.append([p(line['description']),p(line['quantity'],right),p(money(line['unit_pence']),right),p(money(total),right)])
    table=Table(rows,colWidths=[94*mm,18*mm,30*mm,32*mm],repeatRows=1,hAlign='LEFT')
    table.setStyle(TableStyle([('VALIGN',(0,0),(-1,-1),'TOP'),('BACKGROUND',(0,0),(-1,0),colors.HexColor('#edf2ef')),('LINEBELOW',(0,0),(-1,0),.8,accent),('LINEBELOW',(0,1),(-1,-1),.3,colors.HexColor('#d6ded9')),('TOPPADDING',(0,0),(-1,-1),5),('BOTTOMPADDING',(0,0),(-1,-1),4),('LEFTPADDING',(0,0),(-1,-1),6),('RIGHTPADDING',(0,0),(-1,-1),6)]))
    story += [table,Spacer(1,4*mm)]
    totals_table=Table([[p(label,bold if i==2 else base),p(money(amount),right)] for i,(label,amount) in enumerate((('Subtotal',s['subtotal_pence']),('VAT ('+str(s['vat_percent'])+'%)',s['vat_pence']),('Original total',s['total_pence'])))],colWidths=[WIDTH-40*mm,40*mm])
    totals_table.setStyle(TableStyle([('LINEABOVE',(0,2),(-1,2),1,accent),('TOPPADDING',(0,0),(-1,-1),5)]));story.append(KeepTogether(totals_table))
    for label,text in (('Saved notes / terms',s.get('notes')),('Saved payment terms',cfg.get('payment_terms')),('Saved payment instructions',cfg.get('payment_instructions')),('Saved acknowledgement wording',cfg.get('quote_acknowledgement_text')),('Saved footer',cfg.get('footer'))):
        if text:story.extend([p(label,heading),p(text)])
    summary_start=len(story)
    story.extend([p('Current status summary',heading),p('As of '+stamp+'. This section is current information; it does not change the saved content above.',small),p('Status: '+document['status'],bold)])
    if kind=='quote':
        story.append(p('Revision '+str(document['revision'])+(' · current revision' if document['current'] else ' · superseded revision')))
        if document['expired']:story.append(p('The validity date has passed. A new revision is required for a response.'))
        evidence=document.get('response')
        if evidence:
            e=evidence['evidence'];story.append(p('Response: '+evidence['action']+' · '+evidence['created_at']+' · '+e['mode']))
            if e.get('typed_name'):story.append(p('Optional typed acknowledgement: '+e['typed_name']))
            if e.get('acknowledgement'):story.append(p(e['acknowledgement']))
            story.append(p(e['qualification'],small))
    else:
        story += [p('Recorded payments: '+money(document['paid_pence'])),p('Current invoice balance: '+money(document['balance_pence']),bold)]
        if document['status']!='issued':story.append(p('This invoice is not currently payable.'))
        for t in document.get('schedule',[]):
            story.append(p(f"Stage {t['position']}: {t['label']} · original {money(t['amount_pence'])} · recorded {money(t['paid_pence'])} · current remaining {money(t['balance_pence'])} · {t['status']}"))
        for payment in document.get('payments',[]):
            story.append(p('Recorded '+(payment.get('paid_on') or payment['created_at'][:10])+': '+money(payment['amount_pence'])+(' · reversed recording error' if payment['reversed'] else ''),small))
    story += [Spacer(1,4*mm),p('No email sent. No payment collected. Local demonstration only.',small)]
    story[summary_start:]=[KeepTogether(story[summary_start:])]
    stream=io.BytesIO()
    pdf=BaseDocTemplate(stream,pagesize=A4,leftMargin=18*mm,rightMargin=18*mm,topMargin=17*mm,bottomMargin=21*mm,title=title_text+' '+document['reference'],author='Independent local prototype',pageCompression=1)
    def footer(canvas,doc):
        canvas.saveState();canvas.setStrokeColor(accent);canvas.line(18*mm,16*mm,A4[0]-18*mm,16*mm)
        canvas.setFont('SHS-Regular',8);canvas.setFillColor(colors.HexColor('#46574e'))
        canvas.drawString(18*mm,11*mm,'Local demo · '+document['reference'])
        canvas.drawRightString(A4[0]-18*mm,11*mm,'Page '+str(doc.page));canvas.restoreState()
    # Font subsetting is library state; serialize rendering to avoid concurrent font races.
    frame=Frame(pdf.leftMargin,pdf.bottomMargin,pdf.width,pdf.height,id='body',leftPadding=0,rightPadding=0,topPadding=0,bottomPadding=0)
    pdf.addPageTemplates(PageTemplate(id='main',frames=[frame],onPage=footer))
    with _lock:pdf.build(story)
    return stream.getvalue()


def mount_pdf(app,store,session,portal_session):
    def download(s,kind,doc_id,stage_id=None,customer=False):
        with store.tenant(s['business_id']) as db:
            db.execute('BEGIN')
            if customer:
                from .portal_access import check_session
                check_session(db,s)
            doc=get_document(db,kind,doc_id,s['subject'] if customer else None)
            if stage_id and not any(stage['id']==stage_id for stage in doc['schedule']):raise HTTPException(404,'Payment stage not found in this invoice.')
            req=request_snapshot(db,doc_id,stage_id) if stage_id else None
            if stage_id and not doc['issued_at']:raise HTTPException(404,'Issued invoice not found.')
            brand=(req['document'] if req else doc['snapshot'])['business']
            aid=brand.get('logo_id');logo=None
            if aid:
                if not db.execute('SELECT 1 FROM assets WHERE id=?',(aid,)).fetchone():raise HTTPException(409,'Historical document logo is unavailable.')
                path=store.asset_path(s['business_id'],aid)
                if not path.is_file():raise HTTPException(409,'Historical document logo is unavailable.')
                logo=path.read_bytes()
            body=render_document(doc,kind,logo,req)
        name=re.sub(r'[^A-Za-z0-9_-]','_',doc['reference'])[:64]
        if stage_id:name+='-stage-'+str(req['stage']['position'])
        return Response(body,media_type='application/pdf',headers={'Content-Disposition':'attachment; filename="'+name+'-local-demo.pdf"'})

    @app.get('/api/documents/{kind}/{doc_id}/pdf')
    def owner_pdf(kind:str,doc_id:str,request:Request):
        return download(session(request),kind,doc_id)

    @app.get('/api/portal/{slug}/documents/{kind}/{doc_id}/pdf')
    def customer_pdf(slug:str,kind:str,doc_id:str,request:Request):
        return download(portal_session(request,slug),kind,doc_id,customer=True)

    @app.get('/api/invoices/{iid}/schedule/{stage_id}/pdf')
    def owner_request_pdf(iid:str,stage_id:str,request:Request):
        return download(session(request),'invoice',iid,stage_id)

    @app.get('/api/portal/{slug}/invoices/{iid}/schedule/{stage_id}/pdf')
    def customer_request_pdf(slug:str,iid:str,stage_id:str,request:Request):
        return download(portal_session(request,slug),'invoice',iid,stage_id,True)
