from __future__ import annotations

import hashlib
import json
import secrets
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional, Tuple

from .cache import HybridCache

DEFAULT_SESSION_TTL_SECONDS = 60 * 60  # 1 hour
MAX_SESSION_ROWS = 5000
MAX_SESSION_COLUMNS = 150
MAX_SESSION_BYTES = 10 * 1024 * 1024  # 10 MB payload cap


def _utcnow() -> datetime:
    """Return timezone-aware UTC timestamp."""
    return datetime.now(timezone.utc)


def _iso(dt: datetime) -> str:
    """Serialize datetime to ISO string."""
    if dt.tzinfo is None:
        dt = dt.replace(tzinfo=timezone.utc)
    return dt.isoformat().replace("+00:00", "Z")


def _normalize_headers(headers: List[str]) -> List[str]:
    """Ensure headers are strings and unique (appends suffix when duplicated)."""
    normalized: List[str] = []
    counts: Dict[str, int] = {}
    for idx, raw in enumerate(headers):
        header = str(raw).strip() if raw is not None else f"Column {idx + 1}"
        if not header:
            header = f"Column {idx + 1}"
        key = header.lower()
        counts[key] = counts.get(key, 0) + 1
        if counts[key] > 1:
            header = f"{header}_{counts[key]}"
        normalized.append(header)
    return normalized


def _bounded_rows(rows: List[List[Any]], max_cols: int) -> List[List[Any]]:
    """Trim rows to max columns and ensure JSON serializable types."""
    trimmed: List[List[Any]] = []
    for row in rows:
        safe_row: List[Any] = []
        for value in row[:max_cols]:
            if isinstance(value, (str, int, float, bool)) or value is None:
                safe_row.append(value)
            else:
                safe_row.append(str(value))
        trimmed.append(safe_row)
    return trimmed


def _estimate_bytes(headers: List[str], rows: List[List[Any]]) -> int:
    """Approximate payload size in bytes for headers + rows."""
    snapshot = {"headers": headers, "rows": rows}
    return len(json.dumps(snapshot, ensure_ascii=False).encode("utf-8"))


def _snapshot_checksum(headers: List[str], rows: List[List[Any]]) -> str:
    """Create deterministic checksum for snapshot drift detection."""
    payload = {"headers": headers, "rows": rows}
    data = json.dumps(
        payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False
    )
    return hashlib.sha256(data.encode("utf-8")).hexdigest()


class CrmAppendSessionStore:
    """Ephemeral store for CRM Append sessions (Sheets → Dashboard handshake)."""

    def __init__(self, ttl_seconds: int = DEFAULT_SESSION_TTL_SECONDS):
        self.ttl = ttl_seconds
        self.cache = HybridCache(
            namespace="crm_append_sessions", lru_size=256, ttl=self.ttl
        )

    @staticmethod
    def _key(token: str) -> str:
        return f"crm-append-session:{token}"

    async def create_session(
        self,
        *,
        account_id: str,
        user_id: str,
        sheet_id: str,
        sheet_name: str,
        range_a1: str,
        headers: List[str],
        rows: List[List[Any]],
        metadata: Optional[Dict[str, Any]] = None,
    ) -> Tuple[str, Dict[str, Any]]:
        """Persist a sanitized snapshot and return session token + payload."""
        if len(headers) > MAX_SESSION_COLUMNS:
            raise ValueError(f"Session limited to {MAX_SESSION_COLUMNS} columns.")
        if len(rows) > MAX_SESSION_ROWS:
            raise ValueError(f"Session limited to {MAX_SESSION_ROWS} rows.")

        normalized_headers = _normalize_headers(headers)[:MAX_SESSION_COLUMNS]
        trimmed_rows = _bounded_rows(rows, len(normalized_headers))

        estimated_bytes = _estimate_bytes(normalized_headers, trimmed_rows)
        if estimated_bytes > MAX_SESSION_BYTES:
            size_mb = estimated_bytes / (1024 * 1024)
            raise ValueError(
                f"Session payload too large ({size_mb:.1f} MB). Limit is {MAX_SESSION_BYTES / (1024 * 1024):.1f} MB."
            )

        created_at = _utcnow()
        expires_at = created_at + timedelta(seconds=self.ttl)
        checksum = _snapshot_checksum(normalized_headers, trimmed_rows)

        token = secrets.token_urlsafe(24)
        record: Dict[str, Any] = {
            "token": token,
            "account_id": str(account_id),
            "org_id": str(account_id),
            "user_id": str(user_id),
            "sheet": {
                "id": sheet_id,
                "name": sheet_name,
                "range": range_a1,
                "row_count": len(trimmed_rows),
                "headers": normalized_headers,
            },
            "created_at": _iso(created_at),
            "expires_at": _iso(expires_at),
            "snapshot": {
                "rows": trimmed_rows,
                "checksum": checksum,
            },
            "metadata": metadata or {},
        }

        await self.cache.set(self._key(token), record)
        return token, record

    async def get_session(self, token: str) -> Optional[Dict[str, Any]]:
        """Fetch session if present and not expired."""
        if not token:
            return None
        record = await self.cache.get(self._key(token))
        if not record:
            return None
        expires_at = record.get("expires_at")
        if expires_at:
            try:
                expiry = datetime.fromisoformat(expires_at.replace("Z", "+00:00"))
                if expiry < _utcnow():
                    return None
            except ValueError:
                # Ignore invalid expiry strings and keep session accessible
                pass
        return record

    async def update_session(
        self, token: str, updates: Dict[str, Any]
    ) -> Optional[Dict[str, Any]]:
        """Merge updates into session record."""
        record = await self.get_session(token)
        if not record:
            return None

        def _merge(target: Dict[str, Any], patch: Dict[str, Any]) -> Dict[str, Any]:
            for key, value in patch.items():
                if isinstance(value, dict) and isinstance(target.get(key), dict):
                    target[key] = _merge(dict(target[key]), value)
                else:
                    target[key] = value
            return target

        updated = _merge(dict(record), updates)
        await self.cache.set(self._key(token), updated)
        return updated

    async def attach_job(self, token: str, job_id: str, status: str = "queued") -> None:
        """Store job reference for dashboard polling."""
        await self.update_session(
            token,
            {
                "job": {
                    "id": job_id,
                    "status": status,
                    "attached_at": _iso(_utcnow()),
                }
            },
        )


# Singleton for module-level reuse
session_store = CrmAppendSessionStore()
