"""
Rider Substitution Service
==========================
Stores ops-team substitution records: when rider X is absent, rider Y
(the sub) covers X's kitchens and zone for that day.

On each dispatch call, AssignmentService loads today's map and merges the
absent rider's kitchen + zone affinity into the sub rider's profile for
that request only.  No server restart needed.  Automatically reverts the
next day because the lookup is date-keyed.

Storage: SQLite at data/substitutions.db
"""

import logging
import os
import sqlite3
import threading
from datetime import datetime, timezone
from typing import Dict, List, Optional
from zoneinfo import ZoneInfo

logger = logging.getLogger(__name__)

# All date comparisons use IST so the substitution fires on the correct
# calendar day regardless of what timezone the server (Docker/UTC) runs in.
_IST = ZoneInfo("Asia/Kolkata")


def _today_ist() -> str:
    """Current date in IST as YYYY-MM-DD string."""
    return datetime.now(_IST).date().isoformat()

_PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(
    os.path.abspath(__file__)
))))
_DB_PATH = os.path.join(_PROJECT_ROOT, "data", "substitutions.db")

_DDL = """
CREATE TABLE IF NOT EXISTS rider_substitutions (
    sub_date        TEXT    NOT NULL,
    absent_rider_id INTEGER NOT NULL,
    sub_rider_id    INTEGER NOT NULL,
    created_at      TEXT    NOT NULL,
    PRIMARY KEY (sub_date, absent_rider_id)
);
"""


class SubstitutionService:
    def __init__(self, db_path: str = _DB_PATH):
        self._db_path = db_path
        self._lock = threading.Lock()
        # In-memory cache for today's substitution map.
        # Keyed by IST date string so it auto-invalidates at midnight IST.
        self._cache: Dict[int, int] = {}
        self._cache_date: str = ""
        self._init_db()

    def _init_db(self):
        os.makedirs(os.path.dirname(self._db_path), exist_ok=True)
        with sqlite3.connect(self._db_path) as conn:
            conn.execute(_DDL)
            conn.commit()

    def _conn(self) -> sqlite3.Connection:
        conn = sqlite3.connect(self._db_path)
        conn.row_factory = sqlite3.Row
        return conn

    def _invalidate_cache(self):
        """Clear the in-memory cache so the next get_today_map() re-reads from DB."""
        self._cache_date = ""

    def register(self, sub_date: str, absent_rider_id: int, sub_rider_id: int) -> Dict:
        """Register or update a substitution for a given date."""
        created_at = datetime.now(timezone.utc).isoformat()
        with self._lock:
            with self._conn() as conn:
                conn.execute(
                    """
                    INSERT INTO rider_substitutions
                        (sub_date, absent_rider_id, sub_rider_id, created_at)
                    VALUES (?, ?, ?, ?)
                    ON CONFLICT(sub_date, absent_rider_id)
                        DO UPDATE SET sub_rider_id = excluded.sub_rider_id,
                                      created_at   = excluded.created_at
                    """,
                    (sub_date, int(absent_rider_id), int(sub_rider_id), created_at),
                )
                conn.commit()
            self._invalidate_cache()
        logger.info(
            f"[Substitution] Registered: {sub_date} — absent={absent_rider_id} sub={sub_rider_id}"
        )
        return {
            "sub_date": sub_date,
            "absent_rider_id": absent_rider_id,
            "sub_rider_id": sub_rider_id,
            "created_at": created_at,
        }

    def cancel(self, sub_date: str, absent_rider_id: int) -> bool:
        """Remove a substitution record. Returns True if a row was deleted."""
        with self._lock:
            with self._conn() as conn:
                cur = conn.execute(
                    "DELETE FROM rider_substitutions WHERE sub_date=? AND absent_rider_id=?",
                    (sub_date, int(absent_rider_id)),
                )
                conn.commit()
            self._invalidate_cache()
            return cur.rowcount > 0

    def get_today_map(self) -> Dict[int, int]:
        """
        Return {absent_rider_id: sub_rider_id} for today (IST).

        Result is cached in memory — one SQLite read per IST day (or after any
        register/cancel call).  Every dispatch call after the first is a plain
        dict lookup with no DB I/O.
        """
        today = _today_ist()
        with self._lock:
            if self._cache_date == today:
                return dict(self._cache)
            # Cache miss: query DB once, store result
            with self._conn() as conn:
                rows = conn.execute(
                    "SELECT absent_rider_id, sub_rider_id FROM rider_substitutions WHERE sub_date=?",
                    (today,),
                ).fetchall()
            self._cache = {int(r["absent_rider_id"]): int(r["sub_rider_id"]) for r in rows}
            self._cache_date = today
            if self._cache:
                logger.info(f"[Substitution] Cache loaded for {today}: {self._cache}")
            return dict(self._cache)

    def list_all(self, from_date: Optional[str] = None) -> List[Dict]:
        """Return all substitutions on or after from_date (default: today), ordered by date."""
        cutoff = from_date or _today_ist()
        with self._conn() as conn:
            rows = conn.execute(
                """SELECT sub_date, absent_rider_id, sub_rider_id, created_at
                   FROM rider_substitutions
                   WHERE sub_date >= ?
                   ORDER BY sub_date, absent_rider_id""",
                (cutoff,),
            ).fetchall()
        return [dict(r) for r in rows]


_service: Optional[SubstitutionService] = None
_service_lock = threading.Lock()


def get_substitution_service() -> SubstitutionService:
    global _service
    if _service is None:
        with _service_lock:
            if _service is None:
                _service = SubstitutionService()
    return _service
