from __future__ import annotations

import json
from datetime import datetime, timezone, timedelta
from typing import Any, Dict, List, Tuple
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]  # .../kleiner
DATA_DIR = ROOT / "data" / "live_scan"
LIVE_PATH = DATA_DIR / "live_signals.json"
HIST_PATH = DATA_DIR / "intraday_history.json"

# Pflichtenheft-Empfehlung: Entry ~45 Min gültig
ENTRY_VALID_MIN = 45

# Um Flackern zu vermeiden: erst nach X "Misses" als beendet werten
MISSING_GRACE_RUNS = 2

# History-Limit (damit die Datei nicht explodiert)
MAX_HISTORY = 5000


def _now_iso() -> str:
    return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")


def _parse_iso(s: str) -> datetime:
    # erwartet "....Z"
    return datetime.fromisoformat(s.replace("Z", "+00:00"))


def _read_json(path: Path, default: Any) -> Any:
    if not path.exists():
        return default
    raw = path.read_text(encoding="utf-8").strip()
    if not raw:
        return default
    try:
        return json.loads(raw)
    except Exception:
        # wenn mal kaputt geschrieben wurde: lieber safe starten
        return default


def _write_json_atomic(path: Path, obj: Any) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    tmp = path.with_suffix(path.suffix + ".tmp")
    tmp.write_text(json.dumps(obj, indent=2, ensure_ascii=False), encoding="utf-8")
    tmp.replace(path)


def _key(sig: Dict) -> Tuple[str, str]:
    return (str(sig.get("ticker", "")).upper(), str(sig.get("direction", "")).upper())


def update_signal_store(new_signals: List[Dict]) -> Dict[str, Any]:
    """
    - new_signals: Signale vom aktuellen Run (mehrere Ticker möglich)
    Ergebnis:
      - schreibt LIVE_PATH (aktive Signale)
      - verschiebt abgeschlossene/abgelaufene nach HIST_PATH
    """
    now = _now_iso()
    now_dt = _parse_iso(now)

    active: List[Dict] = _read_json(LIVE_PATH, [])
    history: List[Dict] = _read_json(HIST_PATH, [])

    # Index aktive Signale
    active_by_key: Dict[Tuple[str, str], Dict] = {}
    for s in active:
        try:
            active_by_key[_key(s)] = s
        except Exception:
            continue

    seen_keys = set()

    # 1) Upsert: neue Signale in active übernehmen (oder update)
    merged_active: List[Dict] = []
    for ns in new_signals:
        t, d = _key(ns)
        if not t or not d:
            continue

        k = (t, d)
        seen_keys.add(k)

        existing = active_by_key.get(k)
        if existing:
            # Update vorhandenes Signal (bleibt aktiv)
            existing.update(ns)
            existing["ticker"] = t
            existing["direction"] = d
            existing["updated_at"] = now
            existing["valid_until"] = existing.get("expires_at")
            existing["status"] = "active"
            existing["misses"] = 0
            merged_active.append(existing)
        else:
            # Neues Signal
            created_at = now
            expires_at = (now_dt + timedelta(minutes=ENTRY_VALID_MIN)).replace(microsecond=0).isoformat().replace("+00:00", "Z")
            new_item = dict(ns)
            new_item["ticker"] = t
            new_item["direction"] = d
            new_item["created_at"] = created_at
            new_item["updated_at"] = now
            new_item["expires_at"] = expires_at
            new_item["valid_until"] = expires_at
            new_item["status"] = "active"
            new_item["misses"] = 0
            new_item["id"] = f"{t}|{d}|{created_at}"
            merged_active.append(new_item)

    # 2) Alte aktive Signale, die NICHT mehr im neuen Run sind:
    #    - misses++ (grace)
    #    - oder Ablaufzeit überschritten => in Historie verschieben
    for old in active:
        k = _key(old)
        if k in seen_keys:
            continue

        # Ablauf prüfen
        expires_at = old.get("expires_at")
        expired = False
        if isinstance(expires_at, str):
            try:
                expired = _parse_iso(expires_at) <= now_dt
            except Exception:
                expired = False

        # Misses erhöhen
        misses = int(old.get("misses", 0)) + 1
        old["misses"] = misses
        old["updated_at"] = now

        if expired:
            old["status"] = "expired"
            _close_to_history(history, old, now, "entry_window_expired")
        elif misses >= MISSING_GRACE_RUNS:
            old["status"] = "ended"
            _close_to_history(history, old, now, "conditions_no_longer_met")
        else:
            # noch nicht endgültig beenden, bleibt aktiv (grace)
            old["status"] = "active"
            merged_active.append(old)

    # 3) History begrenzen
    if len(history) > MAX_HISTORY:
        history = history[-MAX_HISTORY:]

    _write_json_atomic(LIVE_PATH, merged_active)
    _write_json_atomic(HIST_PATH, history)

    return {
        "now": now,
        "active_count": len(merged_active),
        "history_count": len(history),
        "added_or_updated": len(new_signals),
    }


def _close_to_history(history: List[Dict], sig: Dict, closed_at: str, reason: str) -> None:
    item = dict(sig)
    item["closed_at"] = closed_at
    item["close_reason"] = reason
    history.append(item)
