import json
import os
import time
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, Optional


VALID_STATES = ["CREATED", "VALIDATED", "SENT", "ACKED", "FILLED", "REJECTED", "TIMEOUT", "RECONCILED"]
FINAL_STATES = {"FILLED", "REJECTED", "RECONCILED", "INIT_FAILED"}


def _now() -> str:
    return datetime.now(timezone.utc).isoformat()


_MAX_STATE_SIZE_BYTES = 10 * 1024 * 1024  # 10MB guard — compact earlier to prevent I/O stall

def _load_state(path: Path) -> Dict[str, Any]:
    if not path.exists():
        return {"signals": {}}
    try:
        file_size = path.stat().st_size
        if file_size > _MAX_STATE_SIZE_BYTES:
            # File too large — compact by keeping only recent non-final signals
            import logging
            logging.warning(
                "[execution_state] file too large (%dMB > %dMB limit), compacting...",
                file_size // (1024 * 1024), _MAX_STATE_SIZE_BYTES // (1024 * 1024),
            )
            try:
                with open(str(path), "r", encoding="utf-8") as f:
                    state = json.loads(f.read())
                signals = state.get("signals", {})
                # Keep only non-final signals + last 100 final signals
                final_sorted = sorted(
                    [(k, v) for k, v in signals.items() if isinstance(v, dict) and v.get("state") in FINAL_STATES],
                    key=lambda x: x[1].get("ts", ""), reverse=True,
                )
                non_final = {k: v for k, v in signals.items() if isinstance(v, dict) and v.get("state") not in FINAL_STATES}
                compacted = dict(non_final)
                for k, v in final_sorted[:100]:
                    compacted[k] = v
                state["signals"] = compacted
                _save_state(path, state)
                state["_compacted"] = True
                return state
            except Exception:
                pass  # fall through to normal load
        text = path.read_text(encoding="utf-8").strip()
        if not text:
            return {"signals": {}}
        return json.loads(text)
    except (json.JSONDecodeError, OSError):
        return {"signals": {}}


def _save_state(path: Path, state: Dict[str, Any]) -> None:
    """Atomically write execution state — tmp + fsync + os.replace to prevent
    corruption when gui_backend reads while pipeline writes the 6MB+ file.

    On Windows, os.replace fails with PermissionError (WinError 5) when AV or
    another process holds a brief lock on the target. Retry with backoff before
    giving up so transient locks don't permanently disable state writes.
    """
    path.parent.mkdir(parents=True, exist_ok=True)
    payload = json.dumps(state, ensure_ascii=False, indent=2)
    last_err: Optional[Exception] = None
    for attempt in range(4):
        tmp_path = path.with_name(f".{path.name}.{os.getpid()}.{time.time_ns()}.tmp")
        try:
            with open(tmp_path, "w", encoding="utf-8") as handle:
                handle.write(payload)
                handle.flush()
                os.fsync(handle.fileno())
            os.replace(tmp_path, path)
            return
        except (PermissionError, OSError) as exc:
            last_err = exc
            try:
                if tmp_path.exists():
                    tmp_path.unlink()
            except Exception:
                pass
            time.sleep(0.15 * (2 ** attempt))
    if last_err is not None:
        raise last_err


def create_or_get_signal_state(path: Path, signal_id: str) -> Dict[str, Any]:
    state = _load_state(path)
    signals = state.setdefault("signals", {})
    if signal_id not in signals:
        signals[signal_id] = {
            "signal_id": signal_id,
            "state": "CREATED",
            "history": [{"state": "CREATED", "ts": _now()}],
        }
        _save_state(path, state)
    return signals[signal_id]


def get_signal_state(path: Path, signal_id: str) -> Optional[Dict[str, Any]]:
    state = _load_state(path)
    return state.get("signals", {}).get(signal_id)


def is_final_state(path: Path, signal_id: str) -> bool:
    entry = get_signal_state(path, signal_id)
    if not entry:
        return False
    return str(entry.get("state", "")).upper() in FINAL_STATES


def transition_state(
    path: Path,
    signal_id: str,
    new_state: str,
    reason: Optional[str] = None,
    metadata: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
    if new_state not in VALID_STATES:
        raise ValueError("INVALID_STATE")
    state = _load_state(path)
    signals = state.setdefault("signals", {})
    entry = signals.setdefault(
        signal_id,
        {"signal_id": signal_id, "state": "CREATED", "history": [{"state": "CREATED", "ts": _now()}]},
    )
    entry["state"] = new_state
    event = {"state": new_state, "ts": _now()}
    if reason:
        event["reason"] = reason
    if isinstance(metadata, dict) and metadata:
        event["metadata"] = metadata
    entry.setdefault("history", []).append(event)
    _save_state(path, state)
    return entry


def transition_states(path: Path, signal_id: str, transitions: list[Dict[str, Any]]) -> Dict[str, Any]:
    entry: Optional[Dict[str, Any]] = None
    for item in transitions:
        if not isinstance(item, dict):
            continue
        new_state = str(item.get("state", "") or "").upper()
        if not new_state:
            continue
        entry = transition_state(
            path,
            signal_id,
            new_state,
            reason=str(item.get("reason", "") or "") or None,
            metadata=item.get("metadata", {}) if isinstance(item.get("metadata", {}), dict) else None,
        )
    return entry or create_or_get_signal_state(path, signal_id)


def _report_owned_by_pipeline(report: Dict[str, Any]) -> bool:
    if str(report.get("writer_owner", "") or "").lower() == "pipeline":
        return True
    if str(report.get("request_origin", "") or "").lower() == "pipeline_dispatch":
        return True
    attribution = report.get("attribution", {}) if isinstance(report.get("attribution", {}), dict) else {}
    if str(attribution.get("writer_owner", "") or "").lower() == "pipeline":
        return True
    return False


def _report_ticket(report: Dict[str, Any]) -> int:
    for key in ("ticket", "deal_id", "order_id"):
        value = report.get(key, 0)
        try:
            ticket = int(value or 0)
        except (TypeError, ValueError):
            continue
        if ticket > 0:
            return ticket
    return 0


def apply_execution_report(path: Path, signal_id: str, report: Dict[str, Any]) -> Dict[str, Any]:
    status = str(report.get("status", "")).upper()
    if status == "ACKED":
        return transition_state(path, signal_id, "ACKED")
    if status == "FILLED":
        ticket = _report_ticket(report)
        if ticket <= 0:
            reason = (
                "FILLED_PENDING_MT5_CONFIRM_PIPELINE_OWNED"
                if _report_owned_by_pipeline(report)
                else "FILLED_PENDING_MT5_CONFIRM_INVALID_TICKET"
            )
            return transition_state(path, signal_id, "ACKED", reason=reason)
        return transition_state(path, signal_id, "FILLED")
    if status == "REJECTED":
        return transition_state(path, signal_id, "REJECTED", reason=report.get("reason"))
    if status == "TIMEOUT":
        return transition_state(path, signal_id, "TIMEOUT")
    return transition_state(path, signal_id, "RECONCILED", reason="UNKNOWN_REPORT_STATUS")


def reconcile_open_signals(path: Path) -> Dict[str, Any]:
    state = _load_state(path)
    pending = [
        signal_id
        for signal_id, entry in state.get("signals", {}).items()
        if entry.get("state") in {"SENT", "TIMEOUT", "ACKED"}
    ]
    return {"reconciled_signals": [], "pending_signals": pending}
