"""Explicit, atomic recurrence reviews. Definitions never run in the background."""
import json
from fastapi import Request
from . import recurrence_models as m
from .recurrence_plan import required,fail,build,check_binding,check_rule,rules_for,occurrences_for
from .store import one,many,uid,now
from .scheduling import decode,log as calendar_log
from .workforce import today_london

TABLES=('recurrence_runs','recurrence_history','recurrence_occurrences','recurrence_blocks','recurrence_rules','recurring_contracts')

def insert(db,table,fields):
    db.execute('INSERT INTO '+table+'('+','.join(fields)+') VALUES('+','.join('?' for _ in fields)+')',tuple(fields.values()))
def update(db,table,rid,fields):db.execute('UPDATE '+table+' SET '+','.join(k+'=?' for k in fields)+' WHERE id=?',(*fields.values(),rid))
def get(db,cid):return required(one(db,'SELECT * FROM recurring_contracts WHERE id=?',(cid,)))
def history(db,s,cid,kind,action,before,after,reason='',detail=None):
    insert(db,'recurrence_history',dict(contract_id=cid,kind=kind,record_id=after['id'],action=action,actor_id=s['subject'],membership_id=s['membership_id'],before_value=json.dumps(before) if before else None,after_value=json.dumps(after),detail=json.dumps(detail or {}),reason=reason,created_at=now()))
def guard_generated(db,bid):
    if one(db,'SELECT id FROM recurrence_occurrences WHERE booking_id=?',(bid,)):fail('This is a recurring visit. Open Repeat work and preview a single-visit change, cancellation or restoration.')
def calendar_origin(db,row,staff=False):
    origin=one(db,'SELECT o.id AS occurrence_id,o.contract_id,o.nominal_date,o.state AS occurrence_state,o.overridden,c.name AS contract_name FROM recurrence_occurrences o JOIN recurring_contracts c ON c.id=o.contract_id WHERE o.booking_id=?',(row['id'],))
    if staff:return {**row,'is_recurring':bool(origin),'nominal_date':origin['nominal_date'] if origin else None,'is_override':bool(origin and origin['overridden'])}
    return {**row,'recurrence':origin}
def detail(db,cid):
    c=get(db,cid);occ=list(occurrences_for(db,cid).values())
    for o in occ:o['booking']=decode(one(db,'SELECT * FROM calendar_bookings WHERE id=?',(o['booking_id'],))) if o['booking_id'] else None
    return {**c,'rules':rules_for(db,cid),'blocks':many(db,'SELECT * FROM recurrence_blocks WHERE contract_id=? ORDER BY start_date,id',(cid,)),'occurrences':occ,'runs':many(db,'SELECT id,preview_hash,created_at FROM recurrence_runs WHERE contract_id=? ORDER BY created_at DESC,id',(cid,))}

def reconcile_staff_timers(db,store,business_id):
    """If the final active assignment goes away, cut off at last confirmed time."""
    from .fieldwork import interrupt
    from .identity import assigned
    with store.registry() as registry:
        for row in many(db,"SELECT * FROM field_sessions WHERE state='running'"):
            member=one(registry,'SELECT * FROM memberships WHERE id=? AND user_id=? AND business_id=?',(row['membership_id'],row['author_id'],business_id))
            if member and member['role']=='owner':continue
            ids=[p['id'] for p in json.loads(row['resources'])]
            if not member or not member['enabled'] or member['resource_id'] not in ids or any(not assigned(db,{'resource_id':rid},row['job_id']) for rid in ids):interrupt(db,row,'recurring visit assignment revoked; owner review required')

def apply(db,s,c,plan,payload,store):
    cid=c['id'];stamp=now();run_id=uid();reason=plan['operation'].get('reason','Owner-reviewed generation')
    if plan['new_rule']:
        r={**plan['new_rule'],'content':json.dumps(plan['new_rule']['content']),'author_id':s['subject'],'membership_id':s['membership_id'],'created_at':stamp};insert(db,'recurrence_rules',r);history(db,s,cid,'rule','created',None,r,reason)
    if plan['new_block']:
        b={**plan['new_block'],'created_at':stamp,'updated_at':stamp};insert(db,'recurrence_blocks',b);history(db,s,cid,'block','created',None,b,reason)
    if plan['block_change']:
        b=plan['block_change'];before=one(db,'SELECT * FROM recurrence_blocks WHERE id=?',(b['id'],));update(db,'recurrence_blocks',b['id'],{k:v for k,v in {**b,'updated_at':stamp}.items() if k!='id'});history(db,s,cid,'block','removed',before,one(db,'SELECT * FROM recurrence_blocks WHERE id=?',(b['id'],)),reason)
    changed=[]
    for row in plan['rows']:
        if not row['change']:continue
        t=row['target'];old=one(db,'SELECT * FROM recurrence_occurrences WHERE id=?',(row['occurrence_id'],)) if row['occurrence_id'] else None;bid=row['booking_id'];before=row['current_booking']
        if t['state']=='active':
            fields={**t['candidate'],'archived':0,'updated_at':stamp}
            for key in ('resource_ids','resource_snapshot'):fields[key]=json.dumps(fields[key])
            if bid:update(db,'calendar_bookings',bid,{**fields,'version':before['version']+1})
            else:
                bid=uid();insert(db,'calendar_bookings',{**fields,'id':bid,'created_at':stamp,'version':1})
            calendar_log(db,s,'recurrence.'+plan['operation']['action'],before,decode(one(db,'SELECT * FROM calendar_bookings WHERE id=?',(bid,))))
        elif before and not before['archived']:
            update(db,'calendar_bookings',bid,dict(archived=1,version=before['version']+1,updated_at=stamp));calendar_log(db,s,'recurrence.'+t['state'],before,decode(one(db,'SELECT * FROM calendar_bookings WHERE id=?',(bid,))))
        fields=dict(rule_id=t['rule_id'],rule_snapshot=json.dumps(t['rule_snapshot']),booking_id=bid,state=t['state'],overridden=t['overridden'],exclusion_ids=json.dumps(t['exclusion_ids']),reason=t['reason'],version=old['version']+1 if old else 1,updated_at=stamp)
        oid=old['id'] if old else uid()
        if old:update(db,'recurrence_occurrences',oid,fields)
        else:insert(db,'recurrence_occurrences',dict(id=oid,contract_id=cid,nominal_date=row['nominal_date'],original_rule_id=t['rule_id'],created_at=stamp,**fields))
        history(db,s,cid,'occurrence',plan['operation']['action'],old,one(db,'SELECT * FROM recurrence_occurrences WHERE id=?',(oid,)),reason,{'run_id':run_id});changed.append(oid)
    update(db,'recurring_contracts',cid,{**plan['metadata'],'version':c['version']+1,'updated_at':stamp})
    result={'contract_id':cid,'version':c['version']+1,'run_id':run_id,'changed_occurrence_ids':changed,'retained_for_separate_review':plan['retained_for_separate_review'],'preview_hash':plan['preview_hash']}
    history(db,s,cid,'contract',plan['operation']['action'],c,get(db,cid),reason,{'run_id':run_id,'preview_hash':plan['preview_hash']})
    insert(db,'recurrence_runs',dict(id=run_id,contract_id=cid,actor_id=s['subject'],membership_id=s['membership_id'],preview_hash=plan['preview_hash'],operation=json.dumps(plan['operation']),plan=json.dumps(plan),result=json.dumps(result),acknowledge_retained=int(payload.acknowledge_retained),created_at=stamp))
    reconcile_staff_timers(db,store,s['business_id']);return result

def mount_recurrence(app,store,session,once):
    app.state.recurrence_today=today_london
    def today():return app.state.recurrence_today().isoformat()
    @app.get('/api/recurring')
    def listing(request:Request):
        s=session(request)
        with store.tenant(s['business_id']) as db:return {'items':many(db,'SELECT c.*,j.service AS job_service,u.name AS customer_name FROM recurring_contracts c LEFT JOIN jobs j ON j.id=c.job_id LEFT JOIN customers u ON u.id=c.customer_id ORDER BY c.created_at,c.id'),'today_london':today()}
    @app.post('/api/recurring')
    def create(payload:m.Create,request:Request):
        s=session(request,csrf=True)
        with store.tenant(s['business_id']) as db:
            def action():
                if db.execute('SELECT COUNT(*) FROM recurring_contracts').fetchone()[0]>=100:fail('The local limit is 100 retained recurring definitions.',422)
                values=payload.model_dump();contract={k:values[k] for k in ('kind','job_id','customer_id','name','service_label','note')};j=check_binding(db,contract,active_job=True)
                if j and j['version']!=payload.job_version:fail('The job changed. Refresh before saving its recurring definition.')
                check_rule(db,contract,payload.rule,payload.effective_from,today());stamp=now();cid=uid();contract.update(id=cid,state='active',version=1,created_at=stamp,updated_at=stamp);insert(db,'recurring_contracts',contract)
                r=dict(id=uid(),contract_id=cid,revision=1,effective_from=payload.effective_from,content=json.dumps(values['rule']),author_id=s['subject'],membership_id=s['membership_id'],created_at=stamp);insert(db,'recurrence_rules',r)
                history(db,s,cid,'contract','created',None,contract);history(db,s,cid,'rule','created',None,r);return detail(db,cid)
            return once(db,request,payload.model_dump(),action)
    @app.get('/api/recurring/{contract_id}')
    def retrieve(contract_id:str,request:Request):
        s=session(request)
        with store.tenant(s['business_id']) as db:return detail(db,contract_id)
    @app.post('/api/recurring/{contract_id}/preview')
    def preview(contract_id:str,payload:m.Preview,request:Request):
        s=session(request,csrf=True)
        with store.tenant(s['business_id']) as db:
            db.execute('BEGIN');return build(db,s,get(db,contract_id),payload,today())
    @app.post('/api/recurring/{contract_id}/confirm')
    def confirm(contract_id:str,payload:m.Confirm,request:Request):
        s=session(request,csrf=True)
        with store.tenant(s['business_id']) as db:
            check_binding(db,get(db,contract_id))
            def action():
                c=get(db,contract_id);plan=build(db,s,c,payload,today())
                if plan['preview_hash']!=payload.preview_hash:fail('The preview is stale. Review the current calendar, team and job again.')
                if not plan['can_confirm']:fail('This preview has no confirmable changes. Resolve every listed conflict or error.')
                if plan['retained_for_separate_review'] and not payload.acknowledge_retained:fail('Acknowledge the protected or overridden visits retained for separate review.')
                if db.execute('SELECT COUNT(*) FROM recurrence_runs').fetchone()[0]>=10000:fail('The local limit of 10,000 retained recurrence reviews is reached.',422)
                return apply(db,s,c,plan,payload,store)
            return once(db,request,payload.model_dump(),action)
    @app.get('/api/recurring/{contract_id}/history')
    def audit(contract_id:str,request:Request):
        s=session(request)
        with store.tenant(s['business_id']) as db:
            get(db,contract_id);rows=many(db,'SELECT * FROM recurrence_history WHERE contract_id=? ORDER BY id DESC',(contract_id,))
            for r in rows:
                for k in ('before_value','after_value','detail'):r[k]=json.loads(r[k]) if r[k] else None
            return rows
    @app.get('/api/recurring/{contract_id}/runs/{run_id}')
    def run(contract_id:str,run_id:str,request:Request):
        s=session(request)
        with store.tenant(s['business_id']) as db:
            get(db,contract_id);r=required(one(db,'SELECT * FROM recurrence_runs WHERE id=? AND contract_id=?',(run_id,contract_id)))
            for k in ('operation','plan','result'):r[k]=json.loads(r[k])
            return r
