"""Reconstruct saved research outputs without calling the candidate replay."""
import hashlib
import json
from pathlib import Path
import sys

import numpy as np
import pandas as pd

OUT = Path(__file__).resolve().parent
ROOT = OUT.parents[2]
sys.path.insert(0, str(ROOT))
from tools.run_mlp_learned_exit_experiment import load_input
from tools.run_mlp_volatility_stop_experiment import nominate


def sha(path):
    return hashlib.sha256(Path(path).read_bytes()).hexdigest()


manifest = json.loads((OUT/'manifest.json').read_text())
for name, value in manifest['source_sha256'].items():
    assert sha(ROOT/name) == value, name
for name, value in manifest['outputs_sha256'].items():
    assert sha(OUT/name) == value, name
protected = json.loads((ROOT/'results/mlp_experiments/volatility_stop/protected_hashes.json').read_text())
for name, value in protected.items():
    assert sha(ROOT/name) == value, name
frame, ops, provenance = load_input('4H')
for key in ['data_sha256', 'weights_sha256', 'winner_sha256']:
    assert manifest['inputs'][key] == provenance[key]
metrics = pd.read_csv(OUT/'metrics.csv', float_precision='round_trip')
traces = pd.read_csv(OUT/'bar_traces.csv.gz')
events = pd.read_csv(OUT/'events.csv')
paired = pd.read_csv(OUT/'paired_metrics.csv')
calibration = json.loads((OUT/'calibration.json').read_text())
assert len(metrics) == 40
assert not metrics.duplicated(['window', 'variant']).any()
assert not traces.duplicated(['window', 'variant', 'bar']).any()
assert not events.duplicated(['window', 'variant', 'bar', 'horizon_bars']).any()
selection = json.loads((OUT/'selection.json').read_text())
assert nominate(metrics) == selection
assert sha(OUT/'selection.json') == manifest['selection_sha256_before_evaluation']
# Independent ATR recursion and calibration, without calling volatility().
close = frame.close.to_numpy()
high = frame.high.to_numpy()
low = frame.low.to_numpy()
true_range = np.maximum(high-low, np.maximum(abs(high-np.r_[close[0], close[:-1]]), abs(low-np.r_[close[0], close[:-1]])))
atr = np.full(len(frame), np.nan)
atr[13] = sum(true_range[:14])/14
for i in range(14, len(frame)):
    atr[i] = atr[i-1]+(true_range[i]-atr[i-1])/14
normalized = atr/close
train = (frame.time >= pd.Timestamp('2017-12-01')) & (frame.time < pd.Timestamp('2021-01-01'))
np.testing.assert_allclose(np.median(normalized[train]), calibration['atr_price_median'], rtol=1e-14)
training_trace = traces[(traces.window == 'training') & (traces.variant == 'baseline')]
holding = training_trace.position.shift(1, fill_value=0).astype(bool)
holding_bars = training_trace.loc[holding, 'bar'].to_numpy(int)
assert len(holding_bars) == calibration['holding_bars']
for item in calibration['candidates'].values():
    np.testing.assert_allclose(item['scale'], item['target_median']/np.median(normalized[train]), rtol=1e-14)
    np.testing.assert_allclose(item['matched_fixed_width'], np.clip(item['scale']*normalized[holding_bars-1], .025, .1).mean(), rtol=1e-14)

for (window, variant), trace in traces.groupby(['window', 'variant'], sort=False):
    trace = trace.sort_values('bar')
    metric = metrics[(metrics.window == window) & (metrics.variant == variant)].iloc[0]
    expected_bars = np.flatnonzero((frame.time >= pd.Timestamp(metric.start)) &
                                  (frame.time < pd.Timestamp(metric.end)+pd.Timedelta(days=1)))
    np.testing.assert_array_equal(trace.bar, expected_bars)
    assert trace.time.tolist() == frame.time.iloc[expected_bars].astype(str).tolist()
    active, entry, peak, stop, raw = False, -1, 0., None, False
    fee_returns = []
    previous_entry = False
    exits = []
    for r in trace.itertuples(index=False):
        t = int(r.bar)
        entry_now, exit_now = False, False
        ret = (close[t]/close[t-1]-1) if active else 0.
        if previous_entry:
            ret -= .005
        if active:
            if t != entry+1:
                peak = max(peak, high[t])
            if variant == 'baseline':
                width = .05
            elif variant.startswith('fixed_'):
                width = calibration['candidates'][variant.removeprefix('fixed_')]['matched_fixed_width']
            else:
                width = np.clip(calibration['candidates'][variant]['scale']*normalized[t-1], .025, .1)
            proposed = peak*(1-width)
            stop = proposed if variant == 'baseline' else max(stop, proposed, close[entry]*.95)
            np.testing.assert_allclose([r.stop_price, r.requested_width, r.trade_high], [stop, width, peak], rtol=1e-13)
            score = ops['fallback'][t] if raw else ops['exit'][t]
            if close[t] <= stop or score:
                exit_now, active = True, False
                ret -= .005
                exits.append((t, 'trailing_stop' if close[t] <= stop else 'score_exit'))
        elif ops['entry'][t] and ops['time_ok'][t]:
            entry_now, active, entry, peak = True, True, t, close[t]
            raw = ops['dead_zone'] and ops['score_exit'][t] < ops['exit_threshold']
            if variant == 'baseline':
                width = .05
            elif variant.startswith('fixed_'):
                width = calibration['candidates'][variant.removeprefix('fixed_')]['matched_fixed_width']
            else:
                width = np.clip(calibration['candidates'][variant]['scale']*normalized[t], .025, .1)
            stop = peak*(1-width) if variant == 'baseline' else max(peak*(1-width), peak*.95)
            np.testing.assert_allclose(r.stop_price, stop, rtol=1e-13)
        assert (r.position, r.execute_entry, r.execute_exit) == (int(active), entry_now, exit_now)
        np.testing.assert_allclose(r.strategy_return, ret, rtol=1e-11, atol=2e-16)
        previous_entry = entry_now
        fee_returns.append(ret)
    equity = np.r_[1., np.cumprod(1+np.array(fee_returns))]
    dd = min(equity/np.maximum.accumulate(equity)-1)
    years = (frame.time.iloc[expected_bars[-1]]-frame.time.iloc[expected_bars[0]]+pd.Timedelta(hours=4)).total_seconds()/(365.25*86400)
    calmar = (equity[-1]**(1/years)-1)/abs(dd)
    np.testing.assert_allclose([metric.pnl_pct, metric.dd_pct, metric.calmar, metric.exposure_pct],
                               [(equity[-1]-1)*100, dd*100, calmar, trace.position.mean()*100], rtol=1e-11, atol=1e-11)
    assert metric.entries == trace.execute_entry.sum() and metric.exits == len(exits)
    assert metric.open_at_end == bool(trace.position.iloc[-1])
    recorded = events[(events.window == window) & (events.variant == variant)]
    one_horizon = recorded[recorded.horizon_bars == 6]
    assert list(zip(one_horizon.bar, one_horizon.reason)) == exits
    for horizon, group in recorded.groupby('horizon_bars'):
        retained_end = -1
        for r in group.sort_values('bar').itertuples(index=False):
            t, endpoint = int(r.bar), int(r.bar+r.horizon_bars)
            np.testing.assert_allclose([r.entry_atr_pct, r.exit_atr_pct],
                                       [100*normalized[int(r.entry_bar)], 100*normalized[t]], rtol=1e-13)
            complete = endpoint < len(frame) and frame.time.iloc[endpoint] < pd.Timestamp(metric.end)+pd.Timedelta(days=1)
            gap = complete and bool((frame.time.iloc[t:endpoint+1].diff().dropna() != pd.Timedelta(hours=4)).any())
            valid = complete and not gap
            keep = valid and t > retained_end
            assert (r.valid, r.gap, r.nonoverlap) == (valid, gap, keep)
            if keep:
                retained_end = endpoint
            if valid:
                future = close[t+1:endpoint+1]/close[t:endpoint]-1
                future[-1] -= .005
                advantage = np.prod(1+future)-.995
                np.testing.assert_allclose(r.hold_advantage_pct, advantage*100, atol=1e-12)
                np.testing.assert_allclose(r.terminal_loss_avoided_pct, max(0., -advantage)*100, atol=1e-12)
for r in paired.itertuples(index=False):
    a = metrics[(metrics.window == r.window) & (metrics.variant == r.variant)].iloc[0]
    b = metrics[(metrics.window == r.window) & (metrics.variant == r.control)].iloc[0]
    for name in ['pnl_pct', 'calmar', 'dd_pct', 'entries', 'exposure_pct']:
        np.testing.assert_allclose(getattr(r, 'delta_'+name), a[name]-b[name], atol=1e-12)
for (window, variant), trace in traces.groupby(['window','variant']):
    reference = traces[(traces.window == window) & (traces.variant == 'baseline')]
    np.testing.assert_allclose(trace.paired_bar_return.to_numpy(), trace.strategy_return.to_numpy()-reference.strategy_return.to_numpy(), atol=1e-15)
result = {'status':'passed', 'metric_rows':len(metrics), 'bar_rows':len(traces), 'event_horizon_rows':len(events),
          'paired_metric_rows':len(paired), 'protected_files':len(protected), 'source_files':len(manifest['source_sha256']),
          'checks':['hashes','frozen selection','independent ATR/stop/action reconstruction','fees','calendar metrics',
                    'event censoring and nonoverlap','hold-advantage fee accounting','paired returns','protected files'],
          'audit_source_sha256':sha(__file__)}
(OUT/'audit.json').write_text(json.dumps(result,indent=2)+'\n')
print(json.dumps(result,indent=2))
