"""Independent account-2 flow auditor. No exchange write capability."""
import hashlib
import hmac
import importlib
import json
import os
from pathlib import Path
import sqlite3
import subprocess
import sys
import time
from datetime import datetime, timezone, timedelta
from urllib.parse import urlencode
import requests

ROOT = Path('/opt/charter-live/live_deploy_v2')
HOME = ROOT / 'flow_certifier'
OUTPUT = ROOT / 'logs/flow_certification.json'

def utc():
    return datetime.now(timezone.utc)

def parse(value):
    return datetime.fromisoformat(str(value).replace('Z', '+00:00')).astimezone(timezone.utc)

def evaluate(snapshot, now):
    faults = list(snapshot.get('faults', []))
    heartbeat = snapshot.get('controller', {})
    try:
        age = (now-parse(heartbeat['generated_at'])).total_seconds()
        if not -30 <= age <= 900:
            faults.append({'code':'CONTROLLER_HEARTBEAT_STALE', 'age_seconds':round(age)})
    except (KeyError, ValueError, TypeError):
        faults.append({'code':'CONTROLLER_HEARTBEAT_MISSING'})
    known = set(snapshot.get('known_order_ids', []))
    manual = set(snapshot.get('manual_order_ids', []))
    unmatched = [o for o in snapshot.get('orders', []) if o['orderId'] not in known | manual]
    declared = set(heartbeat.get('unmatched_order_ids', []))
    for order in unmatched:
        faults.append({'code':'ORDER_WITHOUT_WEBHOOK', 'order_id':order['orderId'], 'symbol':order['symbol']})
        # Allow a full controller cycle before calling this a detection miss.
        if int(order.get('createdTime') or 0)/1000 < now.timestamp()-900 and order['orderId'] not in declared:
            faults.append({'code':'CONTROLLER_DETECTION_MISS', 'order_id':order['orderId']})
    owners = {x['symbol']:x for x in snapshot.get('owners', [])}
    for position in snapshot.get('positions', []):
        owner = owners.get(position['symbol'])
        side = {'Buy':'long','Sell':'short'}.get(position['side'])
        if not owner or owner.get('side') != side:
            faults.append({'code':'POSITION_SIDE_MISMATCH','symbol':position['symbol'],
                           'expected':owner.get('side') if owner else None,'actual':side})
    return {'agent':'Flow Certifier Account 2','generated_at':now.isoformat(),
            'valid_until':(now+timedelta(seconds=180)).isoformat(),
            'status':'NOT_CERTIFIED' if faults else 'CERTIFIED',
            'read_only':True,'faults':faults,'checks':snapshot.get('checks',{}),
            'controller_generated_at':heartbeat.get('generated_at'),
            'coverage':'All account-2 assets; received webhooks, live positions and executed Bybit orders in last 24h. TradingView pre-ingress state not certified.',
            'manual_order_ids':list(manual)}

class ReadOnlyBybit:
    def __init__(self):
        from dotenv import dotenv_values
        env=dotenv_values(ROOT/'.env.vps')
        self.key=env['BYBIT_HETZNER_API_KEY'];self.secret=env['BYBIT_HETZNER_SECRET']
        self.session=requests.Session()
    def get(self,path,params):
        if path not in ('/v5/order/history','/v5/position/list'):
            raise RuntimeError('Read-only endpoint not allowed')
        query=urlencode(params);stamp=str(int(time.time()*1000))
        signature=hmac.new(self.secret.encode(),(stamp+self.key+'5000'+query).encode(),hashlib.sha256).hexdigest()
        response=self.session.get('https://api-demo.bybit.com'+path+'?'+query,
            headers={'X-BAPI-API-KEY':self.key,'X-BAPI-SIGN':signature,'X-BAPI-TIMESTAMP':stamp,'X-BAPI-RECV-WINDOW':'5000'},timeout=10)
        response.raise_for_status();data=response.json()
        if data.get('retCode')!=0:raise RuntimeError('Bybit read failure code='+str(data.get('retCode')))
        return data['result']
    def pages(self,path,params):
        rows=[];seen=set()
        for _ in range(20):
            data=self.get(path,params);rows.extend(data['list']);cursor=data.get('nextPageCursor')
            if not cursor:return rows
            if cursor in seen:raise RuntimeError('Repeated pagination cursor')
            seen.add(cursor);params=dict(params,cursor=cursor)
        raise RuntimeError('Incomplete pagination')

def guard_probe():
    sys.path.insert(0,str(ROOT))
    client_module=importlib.import_module('bybit_demo_client')
    class Trap:
        calls=0
        def post(self,*a,**kw):
            self.calls+=1
            raise RuntimeError('PROBE_TRANSPORT_ATTEMPT')
    client=client_module.BybitDemoClient.__new__(client_module.BybitDemoClient)
    client.api_key='probe';client.secret='probe';client.session=Trap()
    for reduce in (False,True):
        try:
            client._request('POST','/v5/order/create',{'category':'linear','symbol':'PROBEUSDT','side':'Sell','qty':'0','reduceOnly':reduce},signed=True)
        except RuntimeError as exc:
            if not str(exc).startswith('ORDER_DISPATCH_BLOCKED'):raise
        else:raise RuntimeError('Dispatch guard did not reject unlinked order')
    if client.session.calls:raise RuntimeError('Dispatch reached transport')
    return True

def snapshot():
    result={'faults':[], 'checks':{}}
    policy=json.loads((HOME/'policy.json').read_text())
    result['manual_order_ids']=policy['manual_order_ids']
    for name,digest in policy['source_sha256'].items():
        path=ROOT/name
        if not path.is_file() or hashlib.sha256(path.read_bytes()).hexdigest()!=digest:
            result['faults'].append({'code':'GUARD_SOURCE_CHANGED','file':name})
    result['checks']['source_hashes_ok']=not result['faults']
    try:
        result['checks']['unlinked_orders_blocked']=guard_probe()
    except Exception as exc:
        result['faults'].append({'code':'GUARD_PROBE_FAILED','error_type':type(exc).__name__})
    state=subprocess.check_output(['systemctl','show','charter-v2-webhook','-p','ActiveState','-p','MainPID','-p','UnitFileState','-p','Restart'],text=True)
    props=dict(line.split('=',1) for line in state.splitlines() if '=' in line)
    result['checks']['webhook_service']=props
    if props.get('ActiveState')!='active' or props.get('UnitFileState')!='enabled':
        result['faults'].append({'code':'WEBHOOK_SERVICE_NOT_PERSISTENT'})
    if props.get('Restart') not in ('always','on-failure'):
        result['faults'].append({'code':'WEBHOOK_RESTART_POLICY_MISSING'})
    try:
        stat=Path('/proc/'+str(int(props['MainPID']))+'/stat').read_text()
        ticks=float(stat.split(') ',1)[1].split()[19])
        boot=float(next(line.split()[1] for line in Path('/proc/stat').read_text().splitlines() if line.startswith('btime ')))
        started=boot+ticks/os.sysconf('SC_CLK_TCK')
        if any((ROOT/name).stat().st_mtime>started+2 for name in policy['source_sha256']):
            result['faults'].append({'code':'WEBHOOK_RUNNING_OLD_CODE'})
        response=requests.get('http://127.0.0.1:5581/health',timeout=3)
        response.raise_for_status()
        if not response.json().get('ok'):raise RuntimeError('Unhealthy webhook')
    except Exception as exc:
        result['faults'].append({'code':'WEBHOOK_RUNTIME_CHECK_FAILED','error_type':type(exc).__name__})
    path=ROOT/'logs/controller_attestation.json'
    if path.exists():
        try:result['controller']=json.loads(path.read_text())
        except (ValueError,OSError):pass
    db=sqlite3.connect('file:'+str(ROOT/'logs/webhook_queue.db')+'?mode=ro',uri=True,timeout=5)
    db.row_factory=sqlite3.Row
    try:
        result['known_order_ids']=[row[0] for row in db.execute('SELECT order_id FROM orders UNION SELECT order_id FROM queue WHERE order_id IS NOT NULL')]
        result['owners']=[dict(row) for row in db.execute('SELECT symbol,side,strategy,entry_order_id FROM position_ownership')]
        for row in db.execute("SELECT request_id,received_at FROM queue WHERE status='pending'"):
            if (utc()-parse(row['received_at'])).total_seconds()>600:
                result['faults'].append({'code':'STALE_PENDING_REQUEST','request_id':row['request_id']})
    finally:db.close()
    client=ReadOnlyBybit()
    result['positions']=[p for p in client.pages('/v5/position/list',{'category':'linear','settleCoin':'USDT','limit':'200'}) if float(p.get('size') or 0)>0]
    result['orders']=[o for o in client.pages('/v5/order/history',{'category':'linear','startTime':str(int((utc()-timedelta(hours=24)).timestamp()*1000)),'limit':'100'}) if float(o.get('cumExecQty') or 0)>0]
    result['checks']['bybit_read_ok']=True
    return result

def main():
    try:report=evaluate(snapshot(),utc())
    except Exception as exc:
        report=evaluate({'faults':[{'code':'CERTIFIER_CHECK_FAILED','error_type':type(exc).__name__}]},utc())
    OUTPUT.parent.mkdir(exist_ok=True)
    temp=OUTPUT.with_suffix('.tmp');temp.write_text(json.dumps(report,indent=2),encoding='utf-8');os.replace(temp,OUTPUT)
    print(json.dumps({'status':report['status'],'generated_at':report['generated_at'],'fault_codes':sorted({f['code'] for f in report['faults']})}))

if __name__=='__main__':main()
