"""
=========================================
CV6 AI Trading OS
Order idempotency cache — Phase 2 (Goals 1 & 3), hardened in Phase 2.5.

Prevents duplicate real-money orders caused by client retries, UI
double-clicks, or a re-evaluated autonomous signal firing twice for the
same intent within a short window. Two dedup strategies are supported:

1. Explicit idempotency_key supplied by the caller — the response for
   the first request with a given key is cached and replayed verbatim
   for any repeat within TTL, without hitting the broker again.
2. Implicit fingerprint dedup — even with no explicit key, an identical
   (broker, symbol, exchange, side, order_type, product, quantity, price)
   tuple seen again within a short window is treated as a duplicate.

PHASE-2.5 FIX: backed by the `idempotency_records` DB table (SQLite),
not an in-process dict — survives a server restart, so a retry that
arrives after a crash/redeploy is still recognized as a duplicate.

PHASE-2.5 FIX: the fingerprint key no longer embeds a fixed time bucket
(`int(time.time() // TTL)`), which let two requests straddling a bucket
boundary slip through undetected. The key is now bucket-free; the TTL
check is a proper sliding window against the stored `created_at`.
"""

import json
import time
from typing import Any, Optional

from loguru import logger

from app.database.session import SessionLocal
from app.execution.idempotency_model import IdempotencyRecord

_EXPLICIT_KEY_TTL = 300.0   # 5 minutes — long enough to cover client retries
_FINGERPRINT_TTL  = 20.0    # sliding window — catches double-clicks/quick retries
                            # without a client-supplied key


def _ttl_for(key: str) -> float:
    return _EXPLICIT_KEY_TTL if key.startswith("EXPLICIT:") else _FINGERPRINT_TTL


def get_cached(key: str) -> Optional[dict]:
    """Return the cached response dict for `key` if it hasn't expired, else None."""
    if not key:
        return None
    db = SessionLocal()
    try:
        rec = db.query(IdempotencyRecord).filter(IdempotencyRecord.key == key).first()
        if rec is None:
            return None
        if time.time() - rec.created_at > _ttl_for(key):
            return None
        try:
            return json.loads(rec.response_json)
        except Exception:
            return None
    except Exception as e:
        # Idempotency is a safety net, not the primary order path — if the DB
        # itself is unavailable, fail open (no cached hit) rather than block
        # all order placement.
        logger.warning(f"[Idempotency] get_cached failed, treating as cache-miss: {e}")
        return None
    finally:
        db.close()


def store(key: str, response: Any) -> None:
    if not key:
        return
    payload = response.model_dump() if hasattr(response, "model_dump") else response
    try:
        payload_json = json.dumps(payload, default=str)
    except Exception as e:
        logger.warning(f"[Idempotency] Could not serialize response for key {key}: {e}")
        return

    db = SessionLocal()
    try:
        existing = db.query(IdempotencyRecord).filter(IdempotencyRecord.key == key).first()
        now = time.time()
        if existing:
            existing.response_json = payload_json
            existing.created_at    = now
        else:
            db.add(IdempotencyRecord(key=key, response_json=payload_json, created_at=now))
        db.commit()
    except Exception as e:
        logger.warning(f"[Idempotency] store failed for key {key}: {e}")
        db.rollback()
    finally:
        db.close()


def purge_expired() -> int:
    """Housekeeping: delete rows past the longer of the two TTLs. Safe to call periodically."""
    cutoff = time.time() - _EXPLICIT_KEY_TTL
    db = SessionLocal()
    try:
        n = db.query(IdempotencyRecord).filter(IdempotencyRecord.created_at < cutoff).delete()
        db.commit()
        return n
    except Exception as e:
        logger.warning(f"[Idempotency] purge_expired failed: {e}")
        db.rollback()
        return 0
    finally:
        db.close()


def fingerprint(broker: str, symbol: str, exchange: str, side: str,
                 order_type: str, product: str, quantity: int, price: float) -> str:
    """
    Build the implicit dedup key for an order's identity — no time bucket
    embedded (Phase 2.5 fix); TTL is enforced via created_at at lookup time.
    """
    return f"FP:{broker}:{symbol}:{exchange}:{side}:{order_type}:{product}:{quantity}:{price}"


def explicit_key(raw_key: str) -> str:
    return f"EXPLICIT:{raw_key}"
