"""Select a training-region center, freeze it, then evaluate later prices.

python parameter_study.py select --csv saved-prices.csv --output selection.json
python parameter_study.py evaluate --selection selection.json --output parameter-study.json
"""
import argparse
import hashlib
import json
from pathlib import Path

import numpy as np
import pandas as pd

from app import Workbench, LOOKBACK_GRID, KC_GRID, GRID_CHUNK, INITIAL_CASH, load_csv, simulate, validate_prices
from data_sources import fetch_history

BASE = dict(dataset="history", period="all", lookback=20, bb_mult=2, kc_mult=1.5,
            fees=5, slippage=2, allocation=95, rsi_filter=False, rsi_threshold=55, split="2024-01-01")
TRAIN_END = pd.Timestamp("2024-01-01", tz="UTC")
REQUEST = dict(provider="yahoo", symbol="BTC-USD", timeframe="1d", start="2018-01-01", end="2026-11-01")


def fingerprint(frame):
    return hashlib.sha256(frame.to_csv(index_label="timestamp").encode()).hexdigest()


def select(frame):
    # Cut before constructing features. No later rows can enter the ranking.
    frame = frame.loc[(frame.index >= pd.Timestamp("2018-01-01", tz="UTC")) & (frame.index < TRAIN_END)]
    if len(frame) < 1000:
        raise ValueError("Supply the complete 2018-2023 training history.")
    bench = Workbench({"history": {"frame": frame}})
    pairs = [(window, kc) for kc in KC_GRID for window in LOOKBACK_GRID]
    grid = {}
    years = (frame.index[-1]+pd.Timedelta(days=1)-frame.index[0]).total_seconds()/(365.25*86400)
    for offset in range(0, len(pairs), GRID_CHUNK):
        chunk = pairs[offset:offset+GRID_CHUNK]
        signals = [bench.signals(BASE, window=window, kc=kc) for window,kc in chunk]
        entries = pd.DataFrame(np.column_stack([s[0] for s in signals]), index=frame.index)
        exits = pd.DataFrame(np.column_stack([s[1] for s in signals]), index=frame.index)
        pf = simulate(frame, entries, exits, BASE)
        equity = pf.value.to_numpy()
        peak = np.maximum.accumulate(np.vstack([np.full((1,len(chunk)), INITIAL_CASH), equity]), axis=0)[1:]
        drawdown = -(equity/peak-1).min(axis=0)
        returns = equity[-1]/INITIAL_CASH-1
        annual = (equity[-1]/INITIAL_CASH)**(1/years)-1
        trades = pf.trades.values
        closed = trades[trades['status']==1]
        counts = np.bincount(closed['col'], minlength=len(chunk))
        for i,pair in enumerate(chunk):
            grid[pair] = dict(lookback=pair[0],kc_mult=pair[1],return_=float(returns[i]),
                              drawdown=float(drawdown[i]),closed_trades=int(counts[i]),
                              calmar=float(annual[i]/drawdown[i]) if drawdown[i]>0 else 0.)
    chosen, candidates = rank_grid(grid)
    return dict(training=dict(start=frame.index[0].isoformat(),end_exclusive=TRAIN_END.isoformat(),bars=len(frame),sha256=fingerprint(frame)),
                configurations=len(pairs),eligible_centers=len(candidates),
                objective="Highest median training Calmar in a complete 3x3 neighborhood. Center has positive return and >=10 closed trades. Ties: center Calmar, lower lookback, lower KC.",
                settings=dict(BASE,lookback=chosen['center']['lookback'],kc_mult=chosen['center']['kc_mult']),
                score=chosen['score'],selected_training=chosen['center'],neighbors=chosen['neighbors'],grid=list(grid.values()))


def rank_grid(grid):
    candidates = []
    for row in range(1,len(KC_GRID)-1):
        for col in range(1,len(LOOKBACK_GRID)-1):
            center = grid[LOOKBACK_GRID[col],KC_GRID[row]]
            if center['closed_trades'] < 10 or center['return_'] <= 0:
                continue
            neighbors = [grid[LOOKBACK_GRID[c],KC_GRID[r]] for r in range(row-1,row+2) for c in range(col-1,col+2)]
            score = float(np.median([n['calmar'] for n in neighbors]))
            if score>0:
                candidates.append(dict(center=center,score=score,neighbors=neighbors))
    if not candidates:
        raise ValueError("No eligible training-region center. No later data was evaluated.")
    chosen = max(candidates,key=lambda x:(x['score'],x['center']['calmar'],-x['center']['lookback'],-x['center']['kc_mult']))
    return chosen, candidates


def evaluate(item, selection):
    frame = item['frame']
    training = frame.loc[(frame.index >= pd.Timestamp("2018-01-01",tz="UTC")) & (frame.index < TRAIN_END)]
    if fingerprint(training) != selection['training']['sha256']:
        raise ValueError("Provider training history was revised. Use the frozen snapshot. Do not silently reselect.")
    rows = []
    fixed = selection['settings']
    configs = [('Original',BASE),('RSI above 55',dict(BASE,rsi_filter=True)),('Selected region',fixed)]
    configs += [(f"Neighbor {n['lookback']} / {n['kc_mult']}",dict(fixed,lookback=n['lookback'],kc_mult=n['kc_mult'])) for n in selection['neighbors']]
    configs += [('Selected / higher costs',dict(fixed,fees=10,slippage=5))]
    for label,start,end in [('2024-2025','2024-01-01','2026-01-01'),('2026','2026-01-01','2026-11-01')]:
        history = frame.loc[frame.index < pd.Timestamp(end,tz='UTC')]
        if len(history.loc[history.index >= pd.Timestamp(start,tz='UTC')]) < 100:
            raise ValueError(f"Insufficient later prices for {label}.")
        bench = Workbench({'history':{**item,'frame':history}})
        for name,settings in configs:
            result = bench.backtest(dict(settings,period='holdout',split=start))
            rows.append(dict(period=label,configuration=name,**{k:result[k] for k in ['settings','bars','first_bar','last_bar','metrics']}))
    return dict(request=REQUEST,provenance=item['provenance'],selection=selection,rows=rows,
                method="Training-only region selection frozen before this study's later evaluation. 2024-2025 was previously inspected for other configurations. 2026 was downloaded after freezing. Retrospective research, not a prospective performance claim.")


def main():
    parser=argparse.ArgumentParser(description=__doc__)
    parser.add_argument('phase',choices=['select','evaluate'])
    parser.add_argument('--csv',type=Path)
    parser.add_argument('--selection',type=Path)
    parser.add_argument('--save-csv',type=Path)
    parser.add_argument('--output',type=Path,required=True)
    args=parser.parse_args()
    if args.output.exists():
        raise ValueError("Output already exists. Choose another path to preserve the research record.")
    if args.phase=='select':
        if args.csv is None:
            raise ValueError("Selection requires a saved training CSV.")
        result=select(load_csv(args.csv))
    else:
        if args.selection is None:
            raise ValueError("Evaluation requires an already frozen selection file.")
        selection=json.loads(args.selection.read_text())
        if args.csv:
            frame=load_csv(args.csv)
            item=dict(frame=frame,provenance=dict(source='Local snapshot',sha256=fingerprint(frame)))
        else:
            item=fetch_history(REQUEST)
            validate_prices(item['frame'])
        if args.save_csv:
            item['frame'].to_csv(args.save_csv,index_label='timestamp')
        result=evaluate(item,selection)
    args.output.write_text(json.dumps(result,indent=2,allow_nan=False)+'\n')
    if args.phase=='select':
        print(json.dumps({k:result[k] for k in ['settings','score','selected_training','eligible_centers']}))
    else:
        print(json.dumps([dict(period=r['period'],configuration=r['configuration'],**r['metrics']) for r in result['rows'][:3]+result['rows'][13:16]],indent=2))


if __name__=='__main__':
    main()
