"""Local account-scoped usage API and operator interface."""
import argparse, csv, hmac, io, json, os, sqlite3
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import urlsplit,parse_qs
from ledger import Ledger, Problem, canonical

HERE=Path(__file__).resolve().parent
DEMO_TOKENS={
 'demo-operator-token':{'account':'demo','role':'operator','actor':'demo-operator'},
 'demo-ingest-token':{'account':'demo','role':'ingest','actor':'demo-ingest'},
 'demo-viewer-token':{'account':'demo','role':'viewer','actor':'demo-viewer'},
 'other-operator-token':{'account':'other','role':'operator','actor':'other-operator'}
}

def make_server(ledger,tokens,port=9090):
    class Handler(BaseHTTPRequestHandler):
        def log_message(self,*args):pass
        def setup(self):
            super().setup();self.connection.settimeout(15)
        def local_request(self):
            host=self.headers.get('Host','')
            if host not in (f'127.0.0.1:{self.server.server_port}',f'localhost:{self.server.server_port}'):
                self.reply(403,{'error':'Localhost access only'});return False
            origin=self.headers.get('Origin')
            if origin and origin!='http://'+host:
                self.reply(403,{'error':'Cross-origin request rejected'});return False
            return True
        def reply(self,status,value,kind='application/json'):
            raw=json.dumps(value,allow_nan=False).encode() if kind=='application/json' else value
            self.send_response(status);self.send_header('Content-Type',kind);self.send_header('Content-Length',str(len(raw)));self.send_header('Cache-Control','no-store');self.send_header('X-Content-Type-Options','nosniff')
            self.send_header('Content-Security-Policy',"default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'")
            self.end_headers();self.wfile.write(raw)
        def identity(self,account,roles):
            supplied=self.headers.get('Authorization','');identity=None
            for token,claims in tokens.items():
                if hmac.compare_digest(supplied,'Bearer '+token):identity=claims
            if identity is None:raise Problem('Token required',401)
            if identity['account']!=account or identity['role'] not in roles:raise Problem('This token cannot perform that account operation',403)
            return identity
        def body(self):
            if self.headers.get('Content-Type','').split(';')[0]!='application/json':raise Problem('Use application/json',415)
            try:n=int(self.headers.get('Content-Length','0'))
            except ValueError:raise Problem('Invalid body length')
            if not 0<n<=100000:raise Problem('Body must be 1-100000 bytes',413)
            try:data=json.loads(self.rfile.read(n),parse_constant=lambda _:(_ for _ in ()).throw(ValueError()))
            except (ValueError,UnicodeError):raise Problem('Invalid JSON')
            if not isinstance(data,dict):raise Problem('JSON object required')
            return data
        def route(self):
            parts=urlsplit(self.path).path.split('/')
            if len(parts)!=5 or parts[1:3]!=['api','accounts']:raise Problem('Not found',404)
            return parts[3],parts[4]
        def do_GET(self):
            if not self.local_request():return
            path=urlsplit(self.path).path
            if path in ('/','/app.js','/style.css'):
                filename={'/':'index.html','/app.js':'app.js','/style.css':'style.css'}[path];kind={'/':'text/html; charset=utf-8','/app.js':'text/javascript; charset=utf-8','/style.css':'text/css; charset=utf-8'}[path]
                self.reply(200,(HERE/'static'/filename).read_bytes(),kind);return
            try:
                account,action=self.route();who=self.identity(account,['operator','viewer','ingest']);query=parse_qs(urlsplit(self.path).query)
                if action=='state':self.reply(200,{**ledger.state(account),'role':who['role']})
                elif action=='reconcile':self.reply(200,ledger.reconcile(account,query.get('period',[''])[0]))
                elif action=='export':self.reply(200,ledger.export(account,query.get('period',[''])[0]))
                elif action=='csv':
                    result=ledger.export(account,query.get('period',[''])[0]);stream=io.StringIO(newline='')
                    fields=['account','period','version','timezone','rule_version','posting_cutoff','posting_seq','ingest_seq','source','event_id','entry_kind','unit','quantity','original_seq','reason']
                    writer=csv.DictWriter(stream,fieldnames=fields);writer.writeheader()
                    from ledger import formatted
                    for entry in result['entries']:
                        row={k:result[k] for k in ['account','period','version','timezone','rule_version','posting_cutoff']}
                        row.update({k:entry.get(k,'') for k in ['posting_seq','ingest_seq','source','event_id','entry_kind','unit','original_seq','reason']});row['quantity']=formatted(int(entry['amount']),entry['unit'])
                        # Prefix spreadsheet formulas in user-entered text; JSON export preserves original bytes.
                        for key in ['source','event_id','reason']:
                            if isinstance(row[key],str) and row[key].startswith(('=','+','-','@','\t','\r')):row[key]="'"+row[key]
                        writer.writerow(row)
                    self.reply(200,stream.getvalue().encode(),'text/csv; charset=utf-8')
                else:raise Problem('Not found',404)
            except Problem as e:self.reply(e.status,{'error':str(e)})
            except sqlite3.Error:self.reply(503,{'error':'Storage unavailable; retry the read'})
        def do_POST(self):
            if not self.local_request():return
            try:
                account,action=self.route();roles=['operator','ingest'] if action=='events' else ['operator'];who=self.identity(account,roles);data=self.body()
                if action=='events':result=ledger.ingest(account,data,who['actor'])
                elif action=='corrections':result=ledger.correct(account,data,who['actor'])
                elif action=='approve':
                    if type(data.get('event_seq')) is not int:raise Problem('event_seq must be an integer')
                    result=ledger.approve(account,data['event_seq'],data.get('period'),who['actor'])
                elif action=='close':result=ledger.close(account,data.get('period'),who['actor'])
                else:raise Problem('Not found',404)
                self.reply(200 if result.get('duplicate') else 201,result)
            except Problem as e:self.reply(e.status,{'error':str(e)})
            except (TypeError,KeyError):self.reply(400,{'error':'Invalid request fields'})
            except sqlite3.Error:self.reply(503,{'error':'Storage unavailable; retry using the same event identity'})
    return ThreadingHTTPServer(('127.0.0.1',port),Handler)

def seed(ledger):
    items=json.loads((HERE/'examples/events.json').read_text())
    for item in items:ledger.ingest('demo',item,'seed')

def main():
    p=argparse.ArgumentParser(description=__doc__);p.add_argument('--db',default='ledger.sqlite');p.add_argument('--port',type=int,default=9090);p.add_argument('--demo',action='store_true');p.add_argument('--tokens');args=p.parse_args()
    if args.demo:tokens=DEMO_TOKENS
    elif args.tokens:tokens=json.loads(Path(args.tokens).read_text())
    else:p.error('Use --demo for synthetic data or provide --tokens path/to/private-tokens.json')
    for key,claims in tokens.items():
        if len(key)<16 or claims.get('role') not in ('operator','viewer','ingest') or not claims.get('account') or not claims.get('actor'):p.error('Invalid token configuration')
    ledger=Ledger(args.db)
    if args.demo:seed(ledger)
    server=make_server(ledger,tokens,args.port);print(f'Usage ledger: http://127.0.0.1:{server.server_port}/')
    if args.demo:print('Account: demo | Operator token: demo-operator-token')
    try:server.serve_forever()
    except KeyboardInterrupt:pass
    finally:server.server_close()
if __name__=='__main__':main()
