"""Audit existing application telemetry and original Journals; never opens the game or writes its data."""
import argparse
import json
import math
from pathlib import Path
from datetime import datetime,timedelta

p = argparse.ArgumentParser(description=__doc__)
p.add_argument('--report', required=True, type=Path)
p.add_argument('--journal', required=True, type=Path)
p.add_argument('--output', required=True, type=Path)
a = p.parse_args()
report = json.loads(a.report.read_text(encoding='utf-8-sig'))
start = datetime.fromisoformat(report['start'])
end = datetime.fromisoformat(report['completedAt'])
rows = 0
first = None
begin = []
commits = []
violations = []
audit_end=end
if report.get('actualArrivalAt'):
    arrival_clock=datetime.fromisoformat(report['actualArrivalAt'])
    if arrival_clock>end+timedelta(seconds=5):
        violations.append('Journal arrival clock exceeded the bounded five-second lead')
    else:
        audit_end=max(end,arrival_clock)
maximum = 0
maximum_axis = 0
maximum_scoop_ratio = 0
minimum_radius_ratio = math.inf
full_power_refill_seconds = 0
full_power_peak_heat = 0
previous_full_power_at = None
refills = {}
orbit_throttles = set()
first_control = {}
input_records = []
latest_inputs = None
cooldown_turns = {}
charge_inputs = []
charge_recovery_frames = 0
cancel_episodes = 0
previous_cancel = False
first_eligible = {}

def clear_target(frame):
    pos, dest = frame['position'], frame['targetPosition']
    delta = [dest[k]-pos[k] for k in ('x','y','z')]
    square = sum(v*v for v in delta)
    for body in frame.get('obstacles', []) + [frame['currentStar']]:
        centre = body['centre']
        projection = min(1, max(0, sum((centre[k]-pos[k])*delta[i] for i,k in enumerate(('x','y','z'))) / square))
        distance = sum((pos[k]+projection*delta[i]-centre[k])**2 for i,k in enumerate(('x','y','z')))
        if distance <= (body['exclusionRadius']*1.05)**2:
            return False
    return True
with Path(str(a.report) + '.jsonl').open(encoding='utf-8') as source:
    for line in source:
        item = json.loads(line)
        if item.get('kind') == 'application-charge-input':
            charge_inputs.append(item)
            if item['request']['heatPercent']>=90 or item['request']['predictedPeak']>95:
                violations.append('Original desired-FSD input exceeded software compatibility thermal bounds')
            continue
        if item.get('kind') == 'application-inputs':
            latest_inputs = item
            input_records.append(item)
            continue
        if item.get('kind') != 'application-frame':
            continue
        frame = item['frame']
        rows += 1
        first = first or frame
        maximum = max(maximum, frame.get('heatPercent') or 0)
        star = frame.get('currentStar')
        if star:
            radius = math.sqrt(sum((frame['position'][k]-star['centre'][k])**2 for k in ('x','y','z')))
            minimum_radius_ratio = min(minimum_radius_ratio, radius/star['exclusionRadius'])
        decision = item.get('decision')
        # FsdPhase.Jumping is 3. A departing frame may already see the next
        # Journal fuel total while still using the previous system geometry.
        if frame.get('adaptiveFlightControl') and frame.get('refuellingToFull') and frame.get('fsd') != 3:
            refill = refills.setdefault(str(frame['systemAddress']), dict(minimum=frame['fuelMain'],maximum=frame['fuelMain'],capacity=frame['fuelTankCapacity']))
            refill['maximum'] = max(refill['maximum'], frame['fuelMain'])
        if str(frame['systemAddress']) in refills and frame.get('fuelMain') is not None:
            refill=refills[str(frame['systemAddress'])]
            refill['maximum']=max(refill['maximum'],frame['fuelMain'])
        if frame.get('scoopRate') is not None and (frame.get('scoopMaximumRate') or 0) > 0:
            maximum_scoop_ratio = max(maximum_scoop_ratio, frame['scoopRate']/frame['scoopMaximumRate'])
        at = datetime.fromisoformat(item['at'])
        full_power = (frame.get('refuellingToFull') and frame.get('scoopActive')
                      and frame['fuelMain'] < frame['fuelTankCapacity']-.02
                      and (frame.get('scoopMaximumRate') or 0) > 0
                      and (frame.get('scoopRate') or 0) >= frame['scoopMaximumRate']*.98)
        if full_power:
            full_power_peak_heat = max(full_power_peak_heat, frame['heatPercent'])
            if previous_full_power_at is not None:
                full_power_refill_seconds += min(.3, max(0, (at-previous_full_power_at).total_seconds()))
            previous_full_power_at = at
        else:
            previous_full_power_at = None
        if not decision:
            continue
        if decision.get('coolingRecovery'):
            charge_recovery_frames += 1
        if decision['cancelCharging'] and not previous_cancel:
            cancel_episodes += 1
        previous_cancel = decision['cancelCharging']
        applied = item.get('applied')
        if applied and applied['sequence'] == frame['sequence']:
            for axis in ('yaw','pitch'):
                if applied.get(axis) is not None:
                    maximum_axis = max(maximum_axis,abs(applied[axis]))
                    if abs(applied[axis]) > 1.000001:
                        violations.append('Native steering exceeded game axis range')
            if applied.get('predictedPeak') is not None and decision['beginCharging'] and applied['predictedPeak'] >= 100:
                violations.append('Native charge forecast would exceed 100 percent')
            if '高功率' in decision['state'] or '圆弧' in decision['state']:
                orbit_throttles.add(applied.get('throttle'))
            first_control.setdefault(str(frame['systemAddress']),dict(at=item['at'],state=decision['state']))
        if frame.get('adaptiveFlightControl'):
            fuel_ready = frame['fuelMain'] >= frame['fuelNeededToNextRefuel']
            if decision['beginCharging'] and not fuel_ready:
                violations.append('Charging began before the persistent fuel target was met')
            if decision['beginCharging'] and str(frame['systemAddress']) in refills:
                refill = refills[str(frame['systemAddress'])]
                refill['departedFuel'] = frame['fuelMain']
                if frame['fuelMain'] < refill['capacity']-.02:
                    violations.append('A required refill departed before filling the tank')
            peak = decision.get('predictedPeakHeat')
            radial = [frame['position'][k]-frame['currentStar']['centre'][k] for k in ('x','y','z')]
            outward = sum(radial[i]*frame['forward'][k] for i,k in enumerate(('x','y','z'))) >= 0
            eligible = (frame['fsd']==0 and frame['canBeginCharge'] and fuel_ready and frame.get('nativeAllowsCharging')
                        and peak is not None and peak <= 95 and clear_target(frame)
                        and (frame['heatPercent']<90 or not frame['outwardDepartureRequired'] or sum(radial[i]*(frame['targetPosition'][k]-frame['position'][k]) for i,k in enumerate(('x','y','z')))>=0)
                        and not decision.get('coolingRecovery'))
            if eligible and not decision['beginCharging']:
                violations.append('An eligible clear/cool/fuelled frame postponed FSD charging')
            if eligible:
                first_eligible.setdefault(str(frame['systemAddress']),dict(at=item['at'],chargeRequested=decision['beginCharging']))
            if frame['fsd']==4 and fuel_ready and clear_target(frame) and frame['heatPercent']<97:
                direction=decision.get('direction')
                target=[frame['targetPosition'][k]-frame['position'][k] for k in ('x','y','z')]
                length=math.sqrt(sum(v*v for v in target))
                dot=sum(direction[k]*target[i]/length for i,k in enumerate(('x','y','z'))) if direction else -1
                if dot<math.cos(math.radians(.01)):
                    violations.append('A clear fuelled cooldown frame postponed turning to the target')
                if decision['beginCharging'] or decision['permitJump']:
                    violations.append('FSD requested before cooldown ended')
                forward_dot=sum(frame['forward'][k]*target[i]/length for i,k in enumerate(('x','y','z')))
                error=math.degrees(math.acos(min(1,max(-1,forward_dot))))
                record=cooldown_turns.setdefault(str(frame['systemAddress']),dict(firstAt=item['at'],firstError=error,frames=0))
                record.update(lastAt=item['at'],lastError=error,frames=record['frames']+1)
        record = dict(at=item['at'], inSupercruise=frame['inSupercruise'],
                      speed=frame['speedMetresPerSecond'], fsd=frame['fsd'],
                      throttle=decision['throttle'], heat=frame['heatPercent'], sequence=frame['sequence'])
        if decision['beginCharging'] and not decision['enterSupercruise']:
            begin.append(record)
            if decision['throttle'] != 1:
                violations.append('Hyperspace charge did not request concurrent full throttle')
        if decision['beginCharging'] or decision['permitJump']:
            flags = item.get('journalContext', {}).get('statusFlags')
            if flags is not None and flags & ((1 << 2) | (1 << 6) | (1 << 9)):
                violations.append('Charging or commit requested while game Status still reported deployed equipment')
            if latest_inputs is not None:
                inputs = latest_inputs['state']
                age = (at-datetime.fromisoformat(inputs['observedAt'])).total_seconds()
                if any(inputs[k] for k in ('hardpoints','cargoScoop','landingGear')) or not -.1 <= age <= .3:
                    violations.append('Charging or commit lacked fresh retracted native-input confirmation')
        if decision['permitJump']:
            f = frame['forward']
            pos = frame['position']
            dest = frame['targetPosition']
            ray = [dest[k] - pos[k] for k in ('x', 'y', 'z')]
            norm = math.sqrt(sum(v*v for v in ray))
            dot = sum(f[k] * ray[i] for i, k in enumerate(('x', 'y', 'z'))) / norm
            record['alignmentDegrees'] = math.degrees(math.acos(min(1, max(-1, dot))))
            commits.append(record)
            if decision['throttle'] != 1 or record['alignmentDegrees'] > 1.001 or decision['predictedPeakHeat'] >= 100:
                violations.append('Jump commit throttle, alignment or thermal guard failed')
events = []
with a.journal.open(encoding='utf-8-sig') as source:
    for line in source:
        item = json.loads(line)
        at = datetime.fromisoformat(item['timestamp'].replace('Z', '+00:00'))
        if start.replace(microsecond=0) <= at <= audit_end and item['event'] in (
                'StartJump', 'FSDJump', 'SupercruiseEntry', 'SupercruiseExit', 'FuelScoop', 'HeatWarning', 'HeatDamage'):
            events.append({k: item[k] for k in ('timestamp', 'event', 'JumpType', 'StarSystem', 'SystemAddress', 'JumpDist', 'FuelUsed', 'FuelLevel', 'Scooped', 'Total') if k in item})
arrivals = [v for v in events if v['event'] == 'FSDJump']
normal_direct = bool(first and not first['inSupercruise'] and first['speedMetresPerSecond'] < .01 and begin
                     and not any(v['event'] == 'SupercruiseEntry' for v in events))
passed = report['passed'] and bool(arrivals) and arrivals[-1]['SystemAddress'] == report['expectedSystem'] and not violations
result = dict(passed=passed, applicationReport=str(a.report.resolve()), journal=str(a.journal.resolve()),
              firstFrame=first, frameRecords=rows, observedPeakHeat=maximum, chargeBegins=begin,
              commits=commits, events=events, normalSpaceZeroSpeedDirect=normal_direct,
              maximumNativeAxis=maximum_axis,maximumScoopRateRatio=maximum_scoop_ratio,refills=refills,
              minimumRadiusRatio=minimum_radius_ratio if math.isfinite(minimum_radius_ratio) else None,
              fullPowerRefillSeconds=full_power_refill_seconds,fullPowerPeakHeat=full_power_peak_heat,
              orbitThrottleCommands=sorted(v for v in orbit_throttles if v is not None),firstControlBySystem=first_control,
              inputConfirmations=len(input_records), firstRetractedInputs=input_records[0] if input_records else None,
              clearFuelledCooldownTurns=cooldown_turns, originalDesiredChargeInputs=charge_inputs,
              firstEligibleChargeBySystem=first_eligible, chargeCancelEpisodes=cancel_episodes, coolingRecoveryFrames=charge_recovery_frames,
              violations=violations, journalWindowEnd=audit_end.isoformat(), gameAccess=False, screenUse=False, journalWrites=0)
if a.output.exists():
    raise FileExistsError('Use a new audit output')
a.output.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding='utf-8')
print(json.dumps(dict(passed=passed, jumps=len(arrivals), observedPeakHeat=maximum,
                     normalSpaceZeroSpeedDirect=normal_direct, violations=violations), ensure_ascii=False))
