"""Independent deterministic reference and golden-file generator for TF MIS 1.0.0.

This implementation intentionally does not import or parse the MQL5 sources.  It mirrors
the locked equations with NumPy and the Python standard library, then writes fixtures,
golden outputs, hashes, parity checks, and a replay/non-repaint check.
"""

from __future__ import annotations

import csv
import hashlib
import json
import math
import time
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path

import numpy as np


ROOT = Path(__file__).resolve().parents[1]
FIXTURES = ROOT / "test-fixtures"
REPORTS = ROOT / "reports"
CURRENCIES = ["USD", "EUR", "GBP", "JPY", "CHF", "CAD", "AUD", "NZD"]
PAIRS = [
    "EURUSD", "GBPUSD", "USDJPY", "USDCHF", "USDCAD", "AUDUSD",
    "NZDUSD", "EURGBP", "EURJPY", "GBPJPY", "AUDJPY", "EURCHF",
]
HORIZONS = np.array([1, 5, 20, 60])
HORIZON_WEIGHTS = np.array([0.20, 0.25, 0.30, 0.25])


def median(values: np.ndarray) -> float:
    return float(np.median(np.asarray(values, dtype=float)))


def mad(values: np.ndarray, floor: float) -> float:
    values = np.asarray(values, dtype=float)
    center = np.median(values)
    return max(1.4826 * float(np.median(np.abs(values - center))), floor)


def sample_std(values: np.ndarray) -> float:
    return float(np.std(values, ddof=1)) if len(values) >= 2 else 0.0


def entropy_of_signs(returns: np.ndarray) -> float:
    tolerance = np.finfo(float).eps * np.maximum(1.0, np.abs(returns))
    counts = np.array([
        np.count_nonzero(returns > tolerance),
        np.count_nonzero(returns < -tolerance),
        np.count_nonzero(np.abs(returns) <= tolerance),
    ])
    probabilities = counts[counts > 0] / len(returns)
    return float(-np.sum(probabilities * np.log(probabilities)) / np.log(3.0))


def true_range(high: np.ndarray, low: np.ndarray, close: np.ndarray, t: int) -> float:
    previous = close[t - 1]
    return float(max(high[t] - low[t], abs(high[t] - previous), abs(low[t] - previous)))


def regime_metrics(high: np.ndarray, low: np.ndarray, close: np.ndarray, t: int) -> tuple[float, float, float, float]:
    efficiencies: list[float] = []
    entropies: list[float] = []
    for period in (20, 50, 100):
        changes = np.diff(close[t - period : t + 1])
        denominator = float(np.abs(changes).sum())
        efficiencies.append(0.0 if denominator <= np.finfo(float).eps else abs(close[t] - close[t - period]) / denominator)
        entropies.append(entropy_of_signs(changes))
    efficiency = median(np.array(efficiencies))
    entropy = median(np.array(entropies))

    def realized(end: int) -> float:
        values = np.diff(np.log(close[end - 20 : end + 1]))
        return sample_std(values)

    current = realized(t)
    historical = np.array([realized(t - j) for j in range(1, 253)])
    tolerance = 1e-12 * np.maximum(1.0, np.maximum(current, historical))
    less = np.count_nonzero(historical < current - tolerance)
    equal = np.count_nonzero(np.abs(historical - current) <= tolerance)
    vol_percentile = 100.0 * (less + 0.5 * equal) / 252.0

    recent = np.array([true_range(high, low, close, k) for k in range(t - 4, t + 1)])
    baseline = np.array([true_range(high, low, close, k) for k in range(t - 64, t - 4)])
    denominator = median(baseline)
    expansion = 1.0 if denominator <= np.finfo(float).eps and median(recent) <= np.finfo(float).eps else (
        float("inf") if denominator <= np.finfo(float).eps else median(recent) / denominator
    )
    return efficiency, entropy, vol_percentile, expansion


def raw_regime(metrics: tuple[float, float, float, float]) -> int:
    efficiency, entropy, vol_percentile, expansion = metrics
    if vol_percentile >= 90.0 or expansion >= 1.8:
        return 4
    if efficiency >= 0.35 and entropy <= 0.90:
        return 1
    if efficiency <= 0.20 and entropy >= 0.95:
        return 2
    return 3


def regime_series(high: np.ndarray, low: np.ndarray, close: np.ndarray) -> list[dict[str, float | int]]:
    output: list[dict[str, float | int]] = []
    previous_state = 0
    previous_raw = 0
    for t in range(272, len(close)):
        metrics = regime_metrics(high, low, close, t)
        raw = raw_regime(metrics)
        state = raw
        if previous_state and raw != 4 and raw != previous_state and previous_raw != raw:
            state = previous_state
        output.append({
            "bar": t,
            "regime": state,
            "raw_regime": raw,
            "efficiency": metrics[0],
            "entropy": metrics[1],
            "vol_percentile": metrics[2],
            "range_expansion": metrics[3],
        })
        previous_state, previous_raw = state, raw
    return output


def make_price_fixture(kind: str, size: int = 1400) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
    rng = np.random.default_rng({"trend": 11, "range": 12, "transition": 13, "shock": 14}[kind])
    if kind == "trend":
        increments = 0.00045 + rng.normal(0.0, 0.00002, size)
    elif kind == "range":
        increments = np.resize(np.array([0.0004, 0.0, -0.0004]), size)
    elif kind == "transition":
        increments = rng.normal(0.0, 0.00035, size)
    else:
        increments = rng.normal(0.0, 0.00010, size)
        increments[-1] = 0.035
    close = 1.10 + np.cumsum(increments)
    width = np.full(size, 0.00035)
    if kind == "shock":
        width[-5:] = 0.003
    high = close + width
    low = close - width
    return high, low, close


def solve_strength(pair_names: list[str], observations: np.ndarray, weights: np.ndarray) -> tuple[np.ndarray, float]:
    design = np.zeros((len(pair_names), 8))
    for row, pair in enumerate(pair_names):
        design[row, CURRENCIES.index(pair[:3])] = 1.0
        design[row, CURRENCIES.index(pair[3:])] = -1.0
    reduced = design[:, :7]
    weighted = np.sqrt(weights)[:, None]
    if np.linalg.matrix_rank(weighted * reduced) < 7:
        raise ValueError("INSUFFICIENT_NETWORK")
    solution, *_ = np.linalg.lstsq(weighted * reduced, np.sqrt(weights) * observations, rcond=None)
    strengths = np.r_[solution, 0.0]
    strengths -= strengths.mean()
    residual = observations - design @ strengths
    rms = math.sqrt(float(np.sum(weights * residual * residual) / np.sum(weights)))
    return strengths, rms


def network_at(log_prices: dict[str, np.ndarray], spreads: dict[str, np.ndarray], t: int) -> tuple[np.ndarray, float]:
    composite = np.zeros(8)
    residual = 0.0
    for horizon, horizon_weight in zip(HORIZONS, HORIZON_WEIGHTS, strict=True):
        observations: list[float] = []
        weights: list[float] = []
        for pair in PAIRS:
            series = log_prices[pair]
            one_bar = np.diff(series[t - 126 : t + 1])
            scale = mad(one_bar, 1e-6) * math.sqrt(float(horizon))
            observations.append((series[t] - series[t - horizon]) / scale)
            spread_history = spreads[pair][t - 125 : t + 1]
            spread_median = median(spread_history)
            weights.append(0.5 if spread_median > 0 and spreads[pair][t] > 3.0 * spread_median else 1.0)
        strengths, horizon_residual = solve_strength(PAIRS, np.array(observations), np.array(weights))
        composite += horizon_weight * strengths
        residual += horizon_weight * horizon_residual
    center = median(composite)
    scale = mad(composite, 1e-6)
    composite = np.clip(composite, center - 3.0 * scale, center + 3.0 * scale)
    composite -= composite.mean()
    return composite, residual


def make_network_fixture(size: int = 1400) -> tuple[dict[str, np.ndarray], dict[str, np.ndarray]]:
    rng = np.random.default_rng(42)
    increments = rng.normal(0.0, 0.00045, (size, 8))
    increments -= increments.mean(axis=1, keepdims=True)
    increments[-80:, 0] += 0.00035
    increments[-80:] -= increments[-80:].mean(axis=1, keepdims=True)
    latent = np.cumsum(increments, axis=0)
    log_prices: dict[str, np.ndarray] = {}
    spreads: dict[str, np.ndarray] = {}
    for pair in PAIRS:
        base = CURRENCIES.index(pair[:3])
        quote = CURRENCIES.index(pair[3:])
        log_prices[pair] = math.log(1.2) + latent[:, base] - latent[:, quote]
        spreads[pair] = rng.integers(8, 20, size).astype(float)
    spreads["GBPJPY"][-1] = 200.0
    return log_prices, spreads


@dataclass
class Position:
    ticket: int
    symbol: str
    side: str
    lots: float
    current: float
    stop: float | None


def portfolio_fixture() -> tuple[list[Position], dict[str, object]]:
    rng = np.random.default_rng(77)
    rates_to_usd = {"USD": 1.0, "EUR": 1.08, "GBP": 1.29, "JPY": 0.0067, "CHF": 1.13, "CAD": 0.74, "AUD": 0.66, "NZD": 0.61}
    positions: list[Position] = []
    net = {currency: 0.0 for currency in CURRENCIES}
    gross = {currency: 0.0 for currency in CURRENCIES}
    stop_risk = 0.0
    unbounded = 0
    for index in range(50):
        pair = PAIRS[index % len(PAIRS)]
        base, quote = pair[:3], pair[3:]
        current = rates_to_usd[base] / rates_to_usd[quote]
        side = "BUY" if index % 2 == 0 else "SELL"
        sign = 1.0 if side == "BUY" else -1.0
        lots = round(float(rng.choice([0.01, 0.05, 0.10, 0.25, 0.50])), 2)
        stop = None if index % 7 == 0 else current * (1.0 - sign * 0.006)
        positions.append(Position(700000 + index, pair, side, lots, current, stop))
        base_value = sign * lots * 100000 * rates_to_usd[base]
        quote_value = -sign * lots * 100000 * current * rates_to_usd[quote]
        net[base] += base_value
        net[quote] += quote_value
        gross[base] += abs(base_value)
        gross[quote] += abs(quote_value)
        if stop is None:
            unbounded += 1
        else:
            stop_risk += max(0.0, -sign * lots * 100000 * (stop - current) * rates_to_usd[quote])
    expected: dict[str, object] = {
        "account_currency": "USD",
        "net": net,
        "gross": gross,
        "stop_risk": stop_risk,
        "unbounded_count": unbounded,
        "position_count": len(positions),
    }
    return positions, expected


def write_csv(path: Path, rows: list[dict[str, object]]) -> None:
    with path.open("w", newline="", encoding="utf-8") as handle:
        writer = csv.DictWriter(handle, fieldnames=list(rows[0]))
        writer.writeheader()
        writer.writerows(rows)


def sha256(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()


def json_default(value: object) -> object:
    if isinstance(value, np.generic):
        return value.item()
    raise TypeError(f"Object of type {type(value).__name__} is not JSON serializable")


def main() -> None:
    started = time.perf_counter()
    FIXTURES.mkdir(parents=True, exist_ok=True)
    REPORTS.mkdir(parents=True, exist_ok=True)
    checks: list[dict[str, object]] = []

    expected_final = {"trend": 1, "range": 2, "transition": 3, "shock": 4}
    golden_regime: list[dict[str, object]] = []
    for kind, expected in expected_final.items():
        high, low, close = make_price_fixture(kind)
        output = regime_series(high, low, close)
        actual = int(output[-1]["regime"])
        checks.append({"test": f"regime_{kind}", "passed": actual == expected, "expected": expected, "actual": actual})
        if kind == "trend":
            golden_regime = output[-1000:]
            rows = [{"bar": i, "high": high[i], "low": low[i], "close": close[i]} for i in range(len(close))]
            write_csv(FIXTURES / "regime_1000plus.csv", rows)

    # Replay causality: later bars cannot change an earlier closed-bar record.
    high, low, close = make_price_fixture("transition", 420)
    baseline = regime_series(high, low, close)
    replay_ok = True
    for prefix in (300, 340, 380, 420):
        replay = regime_series(high[:prefix], low[:prefix], close[:prefix])
        baseline_prefix = [row for row in baseline if int(row["bar"]) < prefix]
        replay_ok &= json.dumps(replay, sort_keys=True) == json.dumps(baseline_prefix, sort_keys=True)
    checks.append({"test": "regime_non_repaint_replay", "passed": replay_ok})
    write_csv(FIXTURES / "golden_regime.csv", golden_regime)

    log_prices, spreads = make_network_fixture()
    network_input_rows = []
    for t in range(1400):
        row: dict[str, object] = {"bar": t}
        for pair in PAIRS:
            row[f"{pair}_log"] = log_prices[pair][t]
            row[f"{pair}_spread"] = spreads[pair][t]
        network_input_rows.append(row)
    write_csv(FIXTURES / "currency_network_input_1400.csv", network_input_rows)
    network_rows: list[dict[str, object]] = []
    for t in range(400, 1400):
        scores, residual = network_at(log_prices, spreads, t)
        network_rows.append({"bar": t, **{currency: scores[i] for i, currency in enumerate(CURRENCIES)}, "residual_rms": residual})
    write_csv(FIXTURES / "golden_currency_network_1000.csv", network_rows)
    last_scores = np.array([network_rows[-1][currency] for currency in CURRENCIES], dtype=float)
    checks.append({"test": "network_sum_zero", "passed": abs(last_scores.sum()) < 1e-10, "actual": float(last_scores.sum())})
    checks.append({"test": "network_usd_rank", "passed": int(np.argmax(last_scores)) == 0, "actual": CURRENCIES[int(np.argmax(last_scores))]})

    # Exact inversion equivalence and leave-one-pair-out rank tests.
    t = 1399
    observations = []
    weights = []
    for pair in PAIRS:
        scale = mad(np.diff(log_prices[pair][t - 126 : t + 1]), 1e-6)
        observations.append((log_prices[pair][t] - log_prices[pair][t - 1]) / scale)
        weights.append(1.0)
    original, _ = solve_strength(PAIRS, np.array(observations), np.array(weights))
    inverted_pairs = PAIRS.copy()
    inverted_observations = np.array(observations)
    inverted_pairs[0] = PAIRS[0][3:] + PAIRS[0][:3]
    inverted_observations[0] *= -1.0
    inverted, _ = solve_strength(inverted_pairs, inverted_observations, np.array(weights))
    checks.append({"test": "network_inverse_pair", "passed": bool(np.allclose(original, inverted, atol=1e-10, rtol=1e-8)), "max_abs": float(np.max(np.abs(original - inverted)))})
    leave_one_ok = True
    stable_cases = 0
    insufficient_cases = 0
    for omitted in range(len(PAIRS)):
        names = [pair for i, pair in enumerate(PAIRS) if i != omitted]
        obs = np.delete(np.array(observations), omitted)
        try:
            solve_strength(names, obs, np.ones(len(names)))
            stable_cases += 1
        except ValueError as exc:
            if str(exc) == "INSUFFICIENT_NETWORK":
                insufficient_cases += 1
            else:
                leave_one_ok = False
    leave_one_ok &= stable_cases + insufficient_cases == len(PAIRS)
    checks.append({"test": "network_leave_one_out", "passed": leave_one_ok, "cases": len(PAIRS), "stable": stable_cases, "insufficient_network": insufficient_cases})

    positions, expected = portfolio_fixture()
    with (FIXTURES / "portfolio_50_positions.json").open("w", encoding="utf-8") as handle:
        json.dump([position.__dict__ for position in positions], handle, indent=2)
    with (FIXTURES / "golden_portfolio.json").open("w", encoding="utf-8") as handle:
        json.dump(expected, handle, indent=2)
    checks.append({"test": "portfolio_50_cases", "passed": len(positions) == 50 and int(expected["unbounded_count"]) == 8})
    checks.append({"test": "portfolio_net_identity", "passed": abs(sum(expected["net"].values())) < 1e-8, "actual": sum(expected["net"].values())})

    # Calendar fixture is interpreted literally as trade-server time; grouping is minute-stable at DST dates.
    with (FIXTURES / "TF_Economic_Events_fixture.csv").open(newline="", encoding="utf-8") as handle:
        calendar_rows = list(csv.DictReader(handle))
    parsed = [datetime.strptime(row["time"], "%Y.%m.%d %H:%M:%S") for row in calendar_rows]
    groups = {(row["time"][:16]) for row in calendar_rows}
    checks.append({"test": "calendar_server_time_roundtrip", "passed": all(moment.strftime("%Y.%m.%d %H:%M:%S") == row["time"] for moment, row in zip(parsed, calendar_rows, strict=True))})
    checks.append({"test": "calendar_same_minute_group", "passed": len(groups) == 4, "groups": len(groups)})

    generated = [
        FIXTURES / "regime_1000plus.csv",
        FIXTURES / "golden_regime.csv",
        FIXTURES / "golden_currency_network_1000.csv",
        FIXTURES / "currency_network_input_1400.csv",
        FIXTURES / "portfolio_50_positions.json",
        FIXTURES / "golden_portfolio.json",
        FIXTURES / "TF_Economic_Events_fixture.csv",
    ]
    report = {
        "suite_version": "1.0.0",
        "generated_at": datetime.now().astimezone().isoformat(),
        "python": __import__("sys").version,
        "numpy": np.__version__,
        "tolerances": {"absolute": 1e-10, "relative": 1e-8},
        "checks": checks,
        "passed": all(bool(check["passed"]) for check in checks),
        "fixture_hashes": {path.name: sha256(path) for path in generated},
        "elapsed_ms": 1000.0 * (time.perf_counter() - started),
    }
    with (REPORTS / "reference-test-report.json").open("w", encoding="utf-8") as handle:
        json.dump(report, handle, indent=2, default=json_default)
    print(json.dumps(report, indent=2, default=json_default))
    if not report["passed"]:
        raise SystemExit(1)


if __name__ == "__main__":
    main()
