"""Local permission fixture: ephemeral identities, public CLI, original-data verification."""
import copy,hashlib,json,os,secrets,sqlite3,subprocess,threading
from collections import defaultdict
from datetime import datetime
from decimal import Decimal
from http.server import BaseHTTPRequestHandler,ThreadingHTTPServer
from pathlib import Path

LAB=Path(__file__).resolve().parents[1]
OUT=LAB/'evidence/chapter-13'
MODELS=LAB/'examples/chapter-13/models'
CLI=LAB/'tools/cli/Scripts/foggy-runtime.exe'
BASE='http://127.0.0.1:18068';NS='chinook-scope'
MODEL='ChinookScopedInvoiceLineQueryModel';BUNDLE='chinook-scope-demo'
STAMP=datetime.now().astimezone().strftime('%Y%m%dT%H%M%S')
TOKENS={role:'Bearer '+secrets.token_urlsafe(32) for role in ['usa','canada','denied']}
AUDIT=[];COMMANDS=[];CASES=[];FAIL_SERVICE=False
OUT.mkdir(exist_ok=True)
def save(path,obj):Path(path).write_text(json.dumps(obj,ensure_ascii=False,indent=2),encoding='utf-8')
def load(path):return json.loads(Path(path).read_text(encoding='utf-8-sig'))
def clean(text):
    for value in TOKENS.values():text=text.replace(value,'[redacted]')
    return text
class PermissionService(BaseHTTPRequestHandler):
    def log_message(self,*args):pass
    def do_POST(self):
        request=json.loads(self.rfile.read(int(self.headers.get('Content-Length','0'))) or b'{}')
        role=next((r for r,t in TOKENS.items() if t==self.headers.get('Authorization')),'anonymous')
        allowed=role in ['usa','canada'] and request.get('model')==MODEL
        AUDIT.append(dict(role=role,action=request.get('action'),allowed=allowed and not FAIL_SERVICE))
        response=dict(allowed=allowed,country='USA' if role=='usa' else 'Canada')
        if FAIL_SERVICE:response=dict(error='demo-permission-service-unavailable')
        body=json.dumps(response).encode()
        self.send_response(503 if FAIL_SERVICE else 200)
        self.send_header('Content-Type','application/json');self.send_header('Content-Length',str(len(body)))
        self.end_headers();self.wfile.write(body)
def run(tag,args,role=None,required=True):
    env=os.environ.copy();env.pop('FOGGY_RUNTIME_AUTHORIZATION',None)
    if role:env['FOGGY_RUNTIME_AUTHORIZATION']=TOKENS[role]
    cmd=[str(CLI),'--base-url',BASE,'--namespace',NS,*map(str,args)]
    proc=subprocess.run(cmd,capture_output=True,encoding='utf-8',timeout=90,env=env)
    path=OUT/f'{STAMP}-{tag}.json';path.write_text(clean(proc.stdout),encoding='utf-8')
    if proc.stderr:path.with_suffix('.stderr.txt').write_text(clean(proc.stderr),encoding='utf-8')
    obj=json.loads(clean(proc.stdout))
    COMMANDS.append(dict(role=role or 'anonymous',arguments=cmd,exitCode=proc.returncode,response=str(path)))
    save(OUT/f'{STAMP}-commands.json',COMMANDS)
    print(json.dumps(dict(step=tag,role=role,exitCode=proc.returncode,success=obj.get('success'),error=obj.get('error')),ensure_ascii=False),flush=True)
    if required:assert proc.returncode==0 and obj.get('success') is True,path
    return obj,str(path)
def query(tag,payload,role,reject=False):
    path=OUT/'payloads'/f'{tag}.json';path.parent.mkdir(exist_ok=True)
    save(path,payload)
    v,vp=run(tag+'-validate',['query','validate',MODEL,'--payload',path],role,required=not reject)
    case=dict(name=tag,role=role or 'anonymous',payload=str(path),validation=vp)
    if reject:
        assert not v['success'];case['executionSkipped']='Validation rejected; no execute.'
        CASES.append(case);save(OUT/'query-manifest.json',CASES);return v
    r,rp=run(tag+'-execute',['query','execute',MODEL,'--payload',path],role)
    case['response']=rp;CASES.append(case);save(OUT/'query-manifest.json',CASES)
    return r['data']
def protected_hashes():
    files=list((LAB/'models').rglob('*.tm'))+list((LAB/'models').rglob('*.qm'))+[LAB/'data/Chinook_Sqlite.sqlite']
    return {str(p):hashlib.sha256(p.read_bytes()).hexdigest() for p in files}
def make_models():
    for directory in ['model','query']:(MODELS/directory).mkdir(parents=True,exist_ok=True)
    tm=(LAB/'models/model/ChinookInvoiceLineModel.tm').read_text(encoding='utf-8').replace("name: 'ChinookInvoiceLineModel'","name: 'ChinookScopedInvoiceLineModel'",1)
    (MODELS/'model/ChinookScopedInvoiceLineModel.tm').write_text(tm,encoding='utf-8')
    qm=(LAB/'models/query/ChinookInvoiceLineQueryModel.qm').read_text(encoding='utf-8')
    qm=qm.replace("loadTableModel('ChinookInvoiceLineModel')","loadTableModel('ChinookScopedInvoiceLineModel')",1).replace("name: 'ChinookInvoiceLineQueryModel'",f"name: '{MODEL}'",1)
    qm=qm.replace("{ ref: line.unitPrice },","{ ref: line.unitPrice, name: 'detailPrice', alias: 'detailPrice' },\n            { ref: line.invoice$id, name: 'billId', alias: 'billId' },",1)
    policy="""    modelPermissions: {
        mode: 'resolver',
        resolver: (context) => {
            const decision = post({
                url: 'http://127.0.0.1:18070/permissions',
                headers: { Authorization: context.authorization },
                body: { namespace: context.namespace, model: context.model, action: context.action },
                responseType: 'map'
            });
            if (decision.allowed !== true) {
                return { allow: false };
            }
            return {
                allow: true,
                attributes: { country: decision.country },
                rowPredicates: [context.predicate.eq(line.invoice$billingCountry, decision.country)],
                policyVersion: 'chapter-13-demo-v1'
            };
        }
    },
    fieldPermissions: {
        defaultVisible: true,
        hiddenFields: [{ fields: ['detailPrice'] }]
    },
"""
    qm=qm.replace('    columnGroups: [',policy+'    columnGroups: [',1)
    (MODELS/'query/ChinookScopedInvoiceLineQueryModel.qm').write_text(qm,encoding='utf-8')
def baseline():
    dbpath=LAB/'data/Chinook_Sqlite.sqlite'
    with sqlite3.connect(dbpath.as_uri()+'?mode=ro&immutable=1',uri=True) as db:
        invoices={r[0]:r[1:] for r in db.execute('SELECT InvoiceId,InvoiceDate,BillingCountry,CustomerId FROM Invoice')}
        lines=list(db.execute('SELECT InvoiceLineId,InvoiceId,UnitPrice,Quantity FROM InvoiceLine'))
    result={}
    for role,country in [('usa','USA'),('canada','Canada')]:
        selected=[r for r in lines if invoices[r[1]][0].startswith('2012-') and invoices[r[1]][1]==country]
        groups=defaultdict(Decimal)
        for _,bill,price,qty in selected:groups[invoices[bill][2]]+=Decimal(str(price))*qty
        result[role]=dict(country=country,amount=str(sum((Decimal(str(r[2]))*r[3] for r in selected),Decimal(0))),lines=len(selected),
            customers={str(k):str(v) for k,v in groups.items()},memberInvoiceIds=sorted(k for k,v in invoices.items() if v[1]==country))
    save(OUT/'baseline.json',result);return result
def main():
    global FAIL_SERVICE
    before=protected_hashes();save(OUT/'before-hashes.json',before)
    service=ThreadingHTTPServer(('127.0.0.1',18070),PermissionService)
    threading.Thread(target=service.serve_forever,daemon=True).start()
    registered=False
    try:
        run('ready',['wait-ready','--timeout-seconds','45'])
        run('capabilities',['capabilities'])
        run('datasource-add',['datasources','add','--name','chinook-sqlite','--type','sqlite','--jdbc-url','jdbc:sqlite:'+str(LAB/'runtime/chapter-13/Chinook.sqlite'),'--replace'])
        run('datasource-bind',['datasources','bind','--namespace',NS,'--data-source','chinook-sqlite'])
        run('datasource-test',['datasources','test','chinook-sqlite'])
        for table in ['InvoiceLine','Invoice','Customer','Track','Album','Artist']:
            run('inspect-'+table,['tables','inspect','--data-source','chinook-sqlite','--table',table])
        make_models();v,_=run('models-validate',['models','validate','--models-dir',MODELS])
        assert v['data']['validFiles']==2 and v['data']['invalidFiles']==0
        run('bundle-add',['bundles','add','--name',BUNDLE,'--path',MODELS,'--replace']);registered=True
        run('refresh',['models','refresh'])
        for role in ['usa','canada','denied',None]:
            r,_=run('list-'+str(role),['models','list'],role)
            text=json.dumps(r['data']);assert (MODEL in text)==(role in ['usa','canada'])
            r,_=run('describe-'+str(role),['models','describe',MODEL],role,required=role in ['usa','canada'])
            if role in ['usa','canada']:
                fields=r['data']['data']['fields'];assert 'detailPrice' not in fields and 'unitPrice' not in fields
                assert {'lineAmount','invoice$billingCountry','billId'}<=fields.keys()
            else:assert r['success'] is False
        facts=baseline()
        date=dict(field='invoice$invoiceDate',op='[)',value=['2012-01-01','2013-01-01'])
        total=dict(columns=['sum(lineAmount) as amount','count(invoiceLineId) as lines'],slice=[date],limit=2)
        for role in ['usa','canada','usa']:
            tag='total-'+role+('-repeat' if role=='usa' and any(c['name']=='total-usa' for c in CASES) else '')
            r=query(tag,total,role);row=r['items'][0]
            assert abs(Decimal(str(row['amount']))-Decimal(facts[role]['amount']))<Decimal('1E-12') and row['lines']==facts[role]['lines']
        for role in ['denied',None]:query('model-denied-'+str(role),total,role,reject=True)
        for field in ['detailPrice','unitPrice']:
            query('hidden-'+field,dict(columns=[field],limit=1),'usa',reject=True)
        cases=[]
        for role in ['usa','canada']:
            rank=dict(columns=['invoice$customerId','sum(lineAmount) as amount'],slice=[date],groupBy=['invoice$customerId'],orderBy=['-amount','invoice$customerId'],limit=500,returnTotal=True)
            full=query('rank-'+role,rank,role)
            assert len(full['items'])==full['total']==len(facts[role]['customers'])
            for row in full['items']:assert abs(Decimal(str(row['amount']))-Decimal(facts[role]['customers'][str(row['invoice$customerId'])]))<Decimal('1E-12')
            for start in [0,3]:
                page=copy.deepcopy(rank);page.update(start=start,limit=3)
                r=query('page-'+role+'-'+str(start),page,role)
                assert r['items']==full['items'][start:start+3] and r['total']==full['total']
            other=copy.deepcopy(rank);other['slice'].append(dict(field='invoice$billingCountry',op='=',value='Canada' if role=='usa' else 'USA'))
            assert query('outside-scope-'+role,other,role)['items']==[]
            m,mp=run('members-bill-'+role,['members','list',MODEL,'billId'],role)
            data=m['data'];assert data['total']==len(facts[role]['memberInvoiceIds'])
            assert all(int(row['id']) in facts[role]['memberInvoiceIds'] for row in data['items'])
            cases.append(dict(role=role,total=data['total'],returned=len(data['items']),response=mp))
        # Artist directory cannot safely represent the billing-country predicate; it must reject.
        unsafe,up=run('members-unsafe-artist',['members','list',MODEL,'albumArtistId'],'usa',required=False)
        assert unsafe['success'] is False
        FAIL_SERVICE=True
        failure=query('permission-service-unavailable',total,'usa',reject=True)
        FAIL_SERVICE=False
        assert protected_hashes()==before
        save(OUT/'acceptance.json',dict(success=True,model=MODEL,namespace=NS,runtimeUrl=BASE,launcherVersion='0.1.30',cliVersion='0.1.25',
            passingQueryCases=sum('response' in c for c in CASES),rejectedQueryCases=sum('executionSkipped' in c for c in CASES),
            facts=facts,memberInvoiceCases=cases,unmappableArtistMembersRejected=True,unmappableResponse=up,
            identityValuesPersisted=False,mainTeachingFilesUnchanged=True,productionIamVerified=False,deniedExecuteRequestsSent=False))
    finally:
        FAIL_SERVICE=False
        if registered:run('bundle-remove',['bundles','remove',BUNDLE])
        service.shutdown();service.server_close()
        save(OUT/'permission-audit.json',AUDIT)
        save(OUT/'fixture-cleanup.json',dict(bundleRemoved=registered,permissionServerStopped=True,mainFilesUnchanged=protected_hashes()==before))

if __name__=='__main__':main()
