"""Private document configuration and immutable local response/request evidence."""
import json
from datetime import datetime
from zoneinfo import ZoneInfo
from fastapi import HTTPException, Request
from pydantic import Field, StrictBool, field_validator, model_validator
from .models import Strict, Version
from .store import one, many, now, digest

SCHEMA='''
CREATE TABLE IF NOT EXISTS sales_settings(id INTEGER PRIMARY KEY CHECK(id=1), content TEXT NOT NULL, version INTEGER NOT NULL);
CREATE TABLE IF NOT EXISTS sales_settings_history(id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT NOT NULL, version INTEGER NOT NULL UNIQUE, actor TEXT NOT NULL, created_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS quote_responses(quote_id TEXT PRIMARY KEY REFERENCES quotes(id), customer_id TEXT NOT NULL REFERENCES customers(id),
 action TEXT NOT NULL CHECK(action IN ('accepted','declined')), evidence TEXT NOT NULL, created_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS payment_request_snapshots(stage_id TEXT PRIMARY KEY REFERENCES payment_stages(id), invoice_id TEXT NOT NULL REFERENCES invoices(id),
 snapshot TEXT NOT NULL, created_at TEXT NOT NULL);
'''
TABLES=('payment_request_snapshots','quote_responses','sales_settings_history','sales_settings')


class Settings(Strict):
    quote_prefix: str = Field(default='Q',pattern=r'^[A-Z][A-Z0-9-]{0,15}$')
    invoice_prefix: str = Field(default='I',pattern=r'^[A-Z][A-Z0-9-]{0,15}$')
    include_year: StrictBool = False
    padding: int = Field(default=4,strict=True,ge=3,le=8)
    payment_instructions: str = Field(default='',max_length=3000)
    quote_acknowledgement_text: str = Field(default='',max_length=3000)

    @model_validator(mode='after')
    def distinct_prefixes(self):
        if self.quote_prefix==self.invoice_prefix:raise ValueError('Quote and invoice prefixes must differ.')
        return self

    @field_validator('payment_instructions','quote_acknowledgement_text')
    @classmethod
    def plain_text(cls,v):
        if any(ord(c)<32 and c not in '\n\t' for c in v):raise ValueError('Use plain printable text.')
        return v.strip()


class SettingsEdit(Settings):
    version: int = Field(strict=True,ge=1)


class Response(Version):
    snapshot_hash: str = Field(pattern=r'^[a-f0-9]{64}$')
    confirmed: StrictBool
    typed_name: str = Field(default='',max_length=200)
    acknowledgement: str = Field(default='',max_length=1000)


def setup(db):
    db.executescript(SCHEMA)
    db.execute('INSERT OR IGNORE INTO sales_settings VALUES(1,?,1)',(json.dumps(Settings().model_dump()),))


def settings(db):
    row=one(db,'SELECT * FROM sales_settings WHERE id=1')
    return {**json.loads(row['content']),'version':row['version']}


def snapshot_hash(content):
    return digest(json.dumps(content,sort_keys=True,separators=(',',':'),ensure_ascii=False))


def allocate_reference(db,kind):
    cfg=settings(db)
    prefix=cfg[kind+'_prefix']+('-'+str(datetime.now(ZoneInfo('Europe/London')).year) if cfg['include_year'] else '')
    value=db.execute('SELECT next_value FROM document_counters WHERE kind=?',(kind,)).fetchone()[0]
    # Counters never reset, including when prefixes/year settings change.
    while True:
        reference=prefix+'-'+str(value).zfill(cfg['padding']);value+=1
        if not db.execute('SELECT 1 FROM quotes WHERE reference=? UNION ALL SELECT 1 FROM invoices WHERE reference=?',(reference,reference)).fetchone():break
    db.execute('UPDATE document_counters SET next_value=? WHERE kind=?',(value,kind))
    return reference


def apply_settings(db,brand):
    cfg=settings(db)
    extra={k:cfg[k] for k in ('payment_instructions','quote_acknowledgement_text') if cfg[k]}
    return {**brand,'document_settings':{**brand.get('document_settings',{}),**extra}}


def record_response(db,q,action,actor,payload=None):
    content=json.loads(q['snapshot'])
    evidence={'reference':q['reference'],'quote_version':q['version'],'snapshot':content,
              'snapshot_hash':snapshot_hash(content),'actor':actor,'mode':'customer' if payload else 'owner-recorded',
              'action_label':'Accept this demo quote' if action=='accepted' else 'Decline this demo quote',
              'owner_wording':content['business'].get('document_settings',{}).get('quote_acknowledgement_text',''),
              'confirmed':payload.confirmed if payload else False,
              'typed_name':payload.typed_name.strip() if payload else '',
              'acknowledgement':payload.acknowledgement.strip() if payload else '',
              'qualification':'Local demonstration record. A typed name is not a certified signature.'}
    db.execute('INSERT INTO quote_responses VALUES(?,?,?,?,?)',(q['id'],q['customer_id'],action,json.dumps(evidence),now()))


def respond(db,qid,payload,action,s):
    from .portal_access import check_session
    from .documents import quote_row,check_version,fail,today,event,accept_quote
    check_session(db,s)
    q=quote_row(db,qid)
    if q['customer_id']!=s['subject'] or not q['shared_at']:fail('Document not found.',404)
    check_version(q,payload.version)
    content=json.loads(q['snapshot'])
    if not payload.confirmed:fail('Review the quote and confirm your chosen action.',422)
    if payload.snapshot_hash!=snapshot_hash(content):fail('The reviewed content does not match this quote. Reload it.')
    if not q['current'] or q['status']!='shared':fail('Only the current shared quote can receive a response.')
    if content.get('valid_until') and content['valid_until']<today():fail('This quote has expired. Ask for a new revision.')
    actor='portal:'+s['subject']
    if action=='accepted':return accept_quote(db,qid,payload.version,actor,s['subject'],response=payload)
    db.execute("UPDATE quotes SET status='declined',version=version+1 WHERE id=?",(qid,))
    record_response(db,q,action,actor,payload)
    event(db,'quote',qid,'declined',q,quote_row(db,qid),'Customer declined the exact shared snapshot. Sources remain reserved until owner revision or cancellation.',actor)
    return {'ok':True}


def freeze_request(db,iid,stage_id,*,legacy_review=False):
    from .documents import get_document,required
    existing=one(db,'SELECT * FROM payment_request_snapshots WHERE stage_id=?',(stage_id,))
    if existing:return json.loads(existing['snapshot'])
    doc=get_document(db,'invoice',iid)
    stage=required(next((s for s in doc['schedule'] if s['id']==stage_id),None))
    if not stage['requested_at']:raise HTTPException(409,'Request this payment stage before creating its document.')
    snapshot={'invoice_id':iid,'reference':doc['reference'],'document':doc['snapshot'],
              'document_hash':snapshot_hash(doc['snapshot']),
              'stage':{k:stage[k] for k in ('id','position','label','amount_pence','due_date','requested_at')},
              'requested_remaining_pence':stage['balance_pence'],'recorded_at':now(),
              'origin':'owner-reviewed legacy request' if legacy_review else 'first explicit local request',
              'qualification':'Part of the existing invoice. Not another invoice, receipt or revenue entry.'}
    db.execute('INSERT INTO payment_request_snapshots VALUES(?,?,?,?)',(stage_id,iid,json.dumps(snapshot),snapshot['recorded_at']))
    return snapshot


def request_snapshot(db,iid,stage_id):
    row=one(db,'SELECT snapshot FROM payment_request_snapshots WHERE stage_id=? AND invoice_id=?',(stage_id,iid))
    if not row:raise HTTPException(409,'This older request needs owner review. Request the stage again to freeze its retained details; this sends nothing.')
    return json.loads(row['snapshot'])


def mount_sales_outputs(app,store,session,once,portal_session):
    @app.get('/api/document-settings')
    def get_settings(request:Request):
        s=session(request)
        with store.tenant(s['business_id']) as db:return settings(db)

    @app.put('/api/document-settings')
    def put_settings(payload:SettingsEdit,request:Request):
        s=session(request,csrf=True)
        with store.tenant(s['business_id']) as db:
            def perform():
                current=settings(db)
                if current['version']!=payload.version:raise HTTPException(409,'Settings changed. Reload before saving.')
                content=json.dumps(payload.model_dump(exclude={'version'}))
                db.execute('UPDATE sales_settings SET content=?,version=version+1 WHERE id=1',(content,))
                db.execute('INSERT INTO sales_settings_history(content,version,actor,created_at) VALUES(?,?,?,?)',(content,payload.version+1,s['subject'],now()))
                return settings(db)
            return once(db,request,payload.model_dump(),perform)

    @app.post('/api/portal/{slug}/quotes/{quote_id}/decline')
    def decline(slug:str,quote_id:str,payload:Response,request:Request):
        s=portal_session(request,slug,True)
        with store.tenant(s['business_id']) as db:return once(db,request,payload.model_dump(),lambda:respond(db,quote_id,payload,'declined',s))
