"""Content-addressed immutable inputs for comparable MLP replay evidence."""
from __future__ import annotations

from dataclasses import dataclass
from functools import cached_property
import hashlib
import io
import json
from pathlib import Path

import numpy as np
import pandas as pd


def digest(value: bytes) -> str:
    return hashlib.sha256(value).hexdigest()


def fingerprint(value: dict) -> str:
    return digest(json.dumps(value, sort_keys=True, separators=(',', ':')).encode())


@dataclass(frozen=True)
class ComparisonSnapshot:
    data: bytes
    artifacts: tuple[tuple[str, bytes], ...]
    manifest_json: str

    @cached_property
    def fingerprint(self) -> str:
        return digest(self.manifest_json.encode())

    @classmethod
    def capture(cls, data_path, weights, *, repo, asset, tf, contract):
        data = Path(data_path).read_bytes()
        sources = ['config.py', 'strategies/mlp_accounting.py',
                   'strategies/strategy_mlp_scores.py', 'strategies/strategy_activation_scores.py',
                   'strategies/library_activation_scores.py', 'strategies/mlp_comparison.py',
                   'strategies/mlp_temporal_features.py', 'tools/run_mlp_deep_sweep.py']
        manifest = dict(asset=asset, timeframe=tf, data_sha256=digest(data), contract=contract,
                        source_hashes={p: digest((Path(repo) / p).read_bytes()) for p in sources})
        return cls(data, tuple((p, Path(p).read_bytes()) for p in sorted(set(weights))),
                   json.dumps(manifest, sort_keys=True, separators=(',', ':')))

    def frame(self):
        frame = pd.read_csv(io.BytesIO(self.data))
        frame.columns = frame.columns.str.lower().str.strip()
        unit = 's' if pd.api.types.is_numeric_dtype(frame.time) else None
        frame['time'] = pd.to_datetime(frame.time, unit=unit, utc=True).dt.tz_localize(None)
        if frame.time.isna().any() or not frame.time.is_monotonic_increasing or frame.time.duplicated().any():
            raise ValueError('Invalid or unordered comparison timestamps')
        # The export's final candle has no proof of finalization.
        return frame.iloc[:-1].copy().reset_index(drop=True)

    @cached_property
    def artifact_hashes(self):
        return {path: digest(raw) for path, raw in self.artifacts}

    @cached_property
    def artifact_bytes(self):
        return dict(self.artifacts)

    def artifact(self, path):
        raw = self.artifact_bytes[path]
        payload = json.loads(raw)
        if not payload['layers']:
            raise ValueError('Empty model layers')
        width = len(payload['feature_cols'])
        for layer in payload['layers']:
            weights = np.asarray(layer['W'], dtype=float)
            bias = np.asarray(layer['b'], dtype=float)
            if weights.ndim != 2 or weights.shape[1] != width or bias.shape != (weights.shape[0],):
                raise ValueError('Incompatible model dimensions')
            width = weights.shape[0]
            if not np.isfinite(np.asarray(layer['W'], dtype=float)).all() or not np.isfinite(np.asarray(layer['b'], dtype=float)).all():
                raise ValueError('Nonfinite model weights')
        if width != 1:
            raise ValueError('Model output dimension must be one')
        return dict(feature_cols=payload['feature_cols'],
                    layers=[(np.asarray(x['W'], dtype=float), np.asarray(x['b'], dtype=float))
                            for x in payload['layers']], sha256=digest(raw))

    def validate_sources(self, repo):
        for path, expected in json.loads(self.manifest_json)['source_hashes'].items():
            if digest((Path(repo) / path).read_bytes()) != expected:
                raise ValueError(f'Comparison source changed: {path}')


def require_replay(row):
    if not row.get('Comparison_Fingerprint') or row.get('Evaluation_Status') != 'complete':
        raise ValueError('Incumbent replay failed or has insufficient coverage')
    for key in ('Calmar Ratio', 'P&L/DD Ratio', 'Total P&L %'):
        if not np.isfinite(float(row.get(key, np.nan))):
            raise ValueError(f'Incumbent replay has invalid {key}')
    return row
