from __future__ import annotations

import argparse
import json
from collections import Counter
from datetime import datetime, timezone, timedelta
from pathlib import Path
from typing import Any


ROOT = Path(__file__).resolve().parents[2]
DEFAULT_JSON = ROOT / "GatherTradingData.json"
DEFAULT_HISTORY = ROOT / "Temp" / "proposal_evaluation_history.json"
KST = timezone(timedelta(hours=9))

# Minimum calendar days before each horizon evaluation fires
T1_MIN_DAYS  = 1
T5_MIN_DAYS  = 7   # ~5 trading days accounting for weekends
T20_MIN_DAYS = 28  # ~20 trading days accounting for weekends


def parse_jsonish(value: Any) -> Any:
    if isinstance(value, (list, dict)):
        return value
    if isinstance(value, str) and value.strip():
        try:
            return json.loads(value)
        except Exception:
            return value
    return value


def rows(value: Any) -> list[dict[str, Any]]:
    parsed = parse_jsonish(value)
    if isinstance(parsed, list):
        return [row for row in parsed if isinstance(row, dict)]
    return []


def num(value: Any) -> float | None:
    try:
        if value is None or value == "":
            return None
        return float(value)
    except Exception:
        return None


def text(value: Any) -> str:
    return str(value or "").strip()


def load_payload(path: Path) -> dict[str, Any]:
    payload = json.loads(path.read_text(encoding="utf-8"))
    if not isinstance(payload, dict):
        raise ValueError("GatherTradingData payload must be an object")
    return payload


def load_history(path: Path) -> dict[str, Any]:
    if not path.exists():
        return {"schema_version": "2026-05-21-proposal-evaluation-v2", "records": []}
    payload = json.loads(path.read_text(encoding="utf-8"))
    if not isinstance(payload, dict):
        return {"schema_version": "2026-05-21-proposal-evaluation-v2", "records": []}
    payload.setdefault("schema_version", "2026-05-21-proposal-evaluation-v2")
    payload.setdefault("records", [])
    if not isinstance(payload["records"], list):
        payload["records"] = []
    return payload


def calendar_days_elapsed(proposal_date: str, current_date_str: str) -> int | None:
    try:
        d0 = datetime.fromisoformat(proposal_date).date()
        d1 = datetime.fromisoformat(current_date_str).date()
        return (d1 - d0).days
    except Exception:
        return None


def current_date(payload: dict[str, Any]) -> str:
    data = payload.get("data") if isinstance(payload.get("data"), dict) else {}
    market_dates: list[str] = []
    for row in rows(data.get("data_feed")) + rows(data.get("core_satellite")):
        raw = text(row.get("Price_Date") or row.get("priceDate"))
        if not raw:
            continue
        normalized = raw.replace(".", "-").replace("/", "-")
        if len(normalized) >= 10:
            market_dates.append(normalized[:10])
    if market_dates:
        return max(market_dates)
    metadata = payload.get("metadata") if isinstance(payload.get("metadata"), dict) else {}
    converted = text(metadata.get("converted_at"))
    if len(converted) >= 10:
        return converted[:10]
    return datetime.now(KST).date().isoformat()


def current_market_map(data: dict[str, Any]) -> dict[str, dict[str, Any]]:
    result: dict[str, dict[str, Any]] = {}
    for row in rows(data.get("data_feed")) + rows(data.get("core_satellite")):
        ticker = text(row.get("Ticker") or row.get("ticker"))
        if not ticker:
            continue
        close = num(row.get("Close") or row.get("close") or row.get("current_price"))
        result[ticker] = {
            "close": close,
            "price_date": text(row.get("Price_Date") or row.get("priceDate") or row.get("Updated_At")),
            "flow_credit": num(row.get("Flow_Credit") or row.get("flowCredit")),
            "rsi14": num(row.get("RSI14") or row.get("rsi14")),
            "ma20": num(row.get("MA20") or row.get("ma20")),
            "volume_ratio": num(row.get("Volume_Ratio_5D") or row.get("AvgVolume_5D")),
            "execution_recommendation_state": text(row.get("Execution_Recommendation_State")),
            "t1_forced_sell_risk_score": num(row.get("T1_Forced_Sell_Risk_Score")),
            "sell_conflict_score": num(row.get("Sell_Conflict_Score")),
        }
    return result


def classify_expected_direction(action: str, order_type: str) -> str:
    raw = f"{action} {order_type}".upper()
    if "BUY" in raw or "ADD" in raw:
        return "UP"
    if "SELL" in raw or "TRIM" in raw or "EXIT" in raw or "STOP" in raw:
        return "DOWN_OR_RISK_REDUCED"
    if "BLOCKED_T1_EXIT_RISK" in raw or "SELL_OR_TRIM_FIRST" in raw or "SELL_CONFLICT" in raw:
        return "DOWN_OR_RISK_REDUCED"
    if "WATCH" in raw:
        return "NEUTRAL_TO_UP"
    return "NEUTRAL"


def build_today_records(payload: dict[str, Any], history: dict[str, Any]) -> list[dict[str, Any]]:
    data = payload.get("data") if isinstance(payload.get("data"), dict) else {}
    h = data.get("_harness_context") if isinstance(data.get("_harness_context"), dict) else {}
    date = current_date(payload)
    existing_ids = {text(row.get("proposal_id")) for row in history.get("records", []) if isinstance(row, dict)}
    market = current_market_map(data)
    decisions = {text(row.get("ticker")): row for row in rows(h.get("decisions_json")) if text(row.get("ticker"))}
    alpha = {text(row.get("ticker")): row for row in rows(h.get("alpha_lead_json")) if text(row.get("ticker"))}
    distribution = {text(row.get("ticker")): row for row in rows(h.get("distribution_risk_json")) if text(row.get("ticker"))}
    quality = {text(row.get("ticker")): row for row in rows(h.get("execution_quality_json")) if text(row.get("ticker"))}

    new_records: list[dict[str, Any]] = []
    for order in rows(h.get("order_blueprint_json")):
        ticker = text(order.get("ticker"))
        if not ticker:
            continue
        action = text((decisions.get(ticker) or {}).get("final_action") or order.get("order_type") or "WATCH")
        order_type = text(order.get("order_type") or "WATCH")
        proposal_id = f"{date}:{ticker}:{order_type}:{action}"
        if proposal_id in existing_ids:
            continue
        m = market.get(ticker, {})
        proposed_close = num(order.get("current_price_krw")) or m.get("close")
        new_records.append({
            "proposal_id": proposal_id,
            "proposal_date": date,
            "ticker": ticker,
            "name": text(order.get("name")),
            "action": action,
            "order_type": order_type,
            "validation_status": text(order.get("validation_status")),
            "expected_direction": classify_expected_direction(action, order_type),
            "proposed_close": proposed_close,
            "proposed_limit_price": num(order.get("limit_price_krw")),
            "proposed_quantity": num(order.get("quantity")),
            "alpha_lead_score": num((alpha.get(ticker) or {}).get("alpha_lead_score")),
            "late_chase_risk_score": num((alpha.get(ticker) or {}).get("late_chase_risk_score")),
            "distribution_risk_score": num((distribution.get(ticker) or {}).get("distribution_risk_score")),
            "execution_quality_status": text((quality.get(ticker) or {}).get("execution_quality_status")),
            "rule_basis": text(order.get("rationale_code")),
            "evaluation_status": "PENDING_T1",
            "result_date": None,
            "result_close": None,
            "next_return_pct": None,
            "outcome": None,
            "error_cause": None,
            "improvement_proposal": None,
            "t5_evaluation_status": "PENDING_T5",
            "t5_result_date": None,
            "t5_return_pct": None,
            "t5_outcome": None,
            "t20_evaluation_status": "PENDING_T20",
            "t20_result_date": None,
            "t20_return_pct": None,
            "t20_outcome": None,
        })
    for row in rows(data.get("core_satellite")):
        ticker = text(row.get("Ticker") or row.get("ticker"))
        state = text(row.get("Execution_Recommendation_State"))
        if not ticker or not state:
            continue
        proposal_id = f"{date}:{ticker}:CORE_SATELLITE:{state}"
        if proposal_id in existing_ids:
            continue
        proposed_close = num(row.get("Close")) or (market.get(ticker) or {}).get("close")
        new_records.append({
            "proposal_id": proposal_id,
            "record_type": "CORE_SATELLITE_CANDIDATE",
            "proposal_date": date,
            "ticker": ticker,
            "name": text(row.get("Name") or row.get("name")),
            "action": state,
            "order_type": "CORE_SATELLITE",
            "validation_status": "CANDIDATE_LEDGER",
            "expected_direction": classify_expected_direction(state, "CORE_SATELLITE"),
            "proposed_close": proposed_close,
            "proposed_limit_price": None,
            "proposed_quantity": None,
            "candidate_quality_grade": text(row.get("Candidate_Quality_Grade")),
            "buy_timing_score": num(row.get("Timing_Score_Entry")),
            "t1_forced_sell_risk_score": num(row.get("T1_Forced_Sell_Risk_Score")),
            "sell_conflict_score": num(row.get("Sell_Conflict_Score")),
            "t1_forced_sell_risk_state": text(row.get("T1_Forced_Sell_Risk_State")),
            "sell_conflict_state": text(row.get("Sell_Conflict_State")),
            "rule_basis": text(row.get("Execution_Recommendation_Reason")),
            "evaluation_status": "PENDING_T1",
            "result_date": None,
            "result_close": None,
            "next_return_pct": None,
            "outcome": None,
            "error_cause": None,
            "improvement_proposal": None,
            "t5_evaluation_status": "PENDING_T5",
            "t5_result_date": None,
            "t5_return_pct": None,
            "t5_outcome": None,
            "t20_evaluation_status": "PENDING_T20",
            "t20_result_date": None,
            "t20_return_pct": None,
            "t20_outcome": None,
        })
    return new_records


def _classify_outcome_(ret: float, expected: str, action: str, thresholds: dict[str, float]) -> tuple[str, str, str]:
    up_pass   = thresholds["up_pass"]
    up_fail   = thresholds["up_fail"]
    down_pass = thresholds["down_pass"]
    down_fail = thresholds["down_fail"]
    nu_lo     = thresholds["nu_lo"]
    nu_hi     = thresholds["nu_hi"]
    nu_fail_lo = thresholds["nu_fail_lo"]
    nu_fail_hi = thresholds["nu_fail_hi"]
    neut_band  = thresholds["neut_band"]

    if expected == "UP":
        pass_case = ret >= up_pass
        fail_case = ret <= up_fail
    elif expected == "DOWN_OR_RISK_REDUCED":
        pass_case = ret <= down_pass
        fail_case = ret >= down_fail
    elif expected == "NEUTRAL_TO_UP":
        pass_case = nu_lo <= ret <= nu_hi
        fail_case = ret <= nu_fail_lo or ret >= nu_fail_hi
    else:
        pass_case = abs(ret) <= neut_band
        fail_case = abs(ret) >= neut_band * 2

    if pass_case:
        return "MATCHED", "THESIS_CONFIRMED", "KEEP_RULE_WEIGHTS"
    if fail_case:
        if "BUY" in action:
            return "MISMATCHED", "LATE_CHASE_OR_FALSE_BREAKOUT", "raise_follow_through_threshold_or_reduce_initial_tranche"
        if "WATCH" in action and ret >= nu_fail_hi:
            return "MISMATCHED", "WATCH_MISSED_MOVE", "lower_pilot_threshold_for_high_quality_low_t1_risk_candidates"
        if "BLOCKED_T1_EXIT_RISK" in action and ret >= down_fail:
            return "MISMATCHED", "T1_RISK_OVERBLOCKED", "recalibrate_t1_forced_sell_risk_weights_after_sample_30"
        if "SELL_CONFLICT" in action or "SELL_OR_TRIM_FIRST" in action:
            return "MISMATCHED", "SELL_CONFLICT_OVERBLOCKED_OR_REBOUND", "compare_cash_floor_need_vs_candidate_alpha_before_blocking_all_new_entries"
        if any(term in action for term in ("SELL", "TRIM", "EXIT")):
            return "MISMATCHED", "REBOUND_AFTER_SELL_SIGNAL", "increase_rebound_holdback_or_split_cash_raise_qty"
        return "MISMATCHED", "WATCH_MISSED_MOVE", "add_candidate_to_alpha_lead_review_and_check_data_lag"
    return "INCONCLUSIVE", "MOVE_WITHIN_NOISE_BAND", "NO_WEIGHT_CHANGE_UNTIL_MORE_SAMPLES"


_HORIZON_THRESHOLDS_ = {
    "t1": dict(up_pass=0.5, up_fail=-1.0, down_pass=0.5, down_fail=1.5,
               nu_lo=-1.5, nu_hi=3.0, nu_fail_lo=-2.5, nu_fail_hi=5.0, neut_band=1.5),
    "t5": dict(up_pass=2.0, up_fail=-3.0, down_pass=1.0, down_fail=4.0,
               nu_lo=-3.0, nu_hi=7.0, nu_fail_lo=-6.0, nu_fail_hi=12.0, neut_band=3.0),
    "t20": dict(up_pass=5.0, up_fail=-8.0, down_pass=2.0, down_fail=10.0,
                nu_lo=-5.0, nu_hi=15.0, nu_fail_lo=-10.0, nu_fail_hi=25.0, neut_band=6.0),
}


def evaluate_record(record: dict[str, Any], date: str, market: dict[str, dict[str, Any]]) -> None:
    proposal_date = text(record.get("proposal_date"))
    if not proposal_date or proposal_date >= date:
        return
    ticker = text(record.get("ticker"))
    current = market.get(ticker)
    if not current or current.get("close") is None:
        return
    proposed = num(record.get("proposed_close"))
    result_close = current["close"]
    if proposed is None or proposed <= 0:
        return
    ret = round((result_close / proposed - 1.0) * 100, 2)
    expected = text(record.get("expected_direction"))
    action = text(record.get("action")).upper()
    elapsed = calendar_days_elapsed(proposal_date, date)

    # T+1 horizon
    if record.get("evaluation_status") == "PENDING_T1" and elapsed is not None and elapsed >= T1_MIN_DAYS:
        outcome, cause, improvement = _classify_outcome_(ret, expected, action, _HORIZON_THRESHOLDS_["t1"])
        record.update({
            "evaluation_status": "EVALUATED_T1",
            "result_date": date,
            "result_close": result_close,
            "next_return_pct": ret,
            "outcome": outcome,
            "error_cause": cause,
            "improvement_proposal": improvement,
        })

    # T+5 horizon
    record.setdefault("t5_evaluation_status", "PENDING_T5")
    if record.get("t5_evaluation_status") == "PENDING_T5" and elapsed is not None and elapsed >= T5_MIN_DAYS:
        t5_outcome, _, _ = _classify_outcome_(ret, expected, action, _HORIZON_THRESHOLDS_["t5"])
        record.update({
            "t5_evaluation_status": "EVALUATED_T5",
            "t5_result_date": date,
            "t5_return_pct": ret,
            "t5_outcome": t5_outcome,
        })

    # T+20 horizon
    record.setdefault("t20_evaluation_status", "PENDING_T20")
    if record.get("t20_evaluation_status") == "PENDING_T20" and elapsed is not None and elapsed >= T20_MIN_DAYS:
        t20_outcome, _, _ = _classify_outcome_(ret, expected, action, _HORIZON_THRESHOLDS_["t20"])
        record.update({
            "t20_evaluation_status": "EVALUATED_T20",
            "t20_result_date": date,
            "t20_return_pct": ret,
            "t20_outcome": t20_outcome,
        })


def _horizon_summary_(records: list[dict[str, Any]], status_key: str, outcome_key: str) -> dict[str, Any]:
    evaluated = [r for r in records if r.get(status_key, "").startswith("EVALUATED_")]
    matched = [r for r in evaluated if r.get(outcome_key) == "MATCHED"]
    mismatched = [r for r in evaluated if r.get(outcome_key) == "MISMATCHED"]
    rets = [r.get(outcome_key.replace("_outcome", "_return_pct")) for r in evaluated
            if isinstance(r.get(outcome_key.replace("_outcome", "_return_pct")), (int, float))]
    return {
        "evaluated_count": len(evaluated),
        "matched_count": len(matched),
        "mismatched_count": len(mismatched),
        "match_rate_pct": round(len(matched) / len(evaluated) * 100, 2) if evaluated else None,
        "avg_return_pct": round(sum(rets) / len(rets), 2) if rets else None,
    }


def summarize(records: list[dict[str, Any]]) -> dict[str, Any]:
    t1_eval  = [r for r in records if r.get("evaluation_status") == "EVALUATED_T1"]
    t1_miss  = [r for r in t1_eval if r.get("outcome") == "MISMATCHED"]
    t1_match = [r for r in t1_eval if r.get("outcome") == "MATCHED"]
    error_counts = Counter(text(r.get("error_cause")) for r in t1_miss if text(r.get("error_cause")))
    improvement_rows = [r for r in t1_eval if text(r.get("improvement_proposal"))]
    improvement_counts = Counter(text(r.get("improvement_proposal")) for r in improvement_rows)
    improvement_coverage_pct = round(len(improvement_rows) / len(t1_eval) * 100, 2) if t1_eval else None
    return {
        "evaluated_count": len(t1_eval),
        "matched_count": len(t1_match),
        "mismatched_count": len(t1_miss),
        "match_rate_pct": round(len(t1_match) / len(t1_eval) * 100, 2) if t1_eval else None,
        "top_error_causes": sorted({text(r.get("error_cause")) for r in t1_miss if r.get("error_cause")}),
        "error_cause_counts": dict(error_counts.most_common()),
        "improvement_proposal_counts": dict(improvement_counts.most_common()),
        "improvement_coverage_pct": improvement_coverage_pct,
        "t5_horizon": _horizon_summary_(records, "t5_evaluation_status", "t5_outcome"),
        "t20_horizon": _horizon_summary_(records, "t20_evaluation_status", "t20_outcome"),
        "last_updated": datetime.now(KST).isoformat(timespec="seconds"),
    }


def main() -> int:
    parser = argparse.ArgumentParser(description="Persist daily proposal history and evaluate prior proposals against current market data.")
    parser.add_argument("--json", default=str(DEFAULT_JSON))
    parser.add_argument("--history", default=str(DEFAULT_HISTORY))
    args = parser.parse_args()

    json_path = Path(args.json)
    history_path = Path(args.history)
    if not json_path.is_absolute():
        json_path = ROOT / json_path
    if not history_path.is_absolute():
        history_path = ROOT / history_path

    payload = load_payload(json_path)
    data = payload.get("data") if isinstance(payload.get("data"), dict) else {}
    date = current_date(payload)
    history = load_history(history_path)
    market = current_market_map(data)

    records = [row for row in history.get("records", []) if isinstance(row, dict)]
    for record in records:
        evaluate_record(record, date, market)
    records.extend(build_today_records(payload, history))
    records.sort(key=lambda row: (text(row.get("proposal_date")), text(row.get("ticker")), text(row.get("proposal_id"))))

    history["records"] = records
    history["summary"] = summarize(records)
    history_path.parent.mkdir(parents=True, exist_ok=True)
    history_path.write_text(json.dumps(history, ensure_ascii=False, indent=2), encoding="utf-8")
    print(f"PROPOSAL_EVALUATION_HISTORY_UPDATED: {history_path} records={len(records)}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
