# -*- coding: utf-8 -*-
"""Two-phase resource-history dataset cache.

Primary query (POST /query) → Oracle → cache full per-resource × per-day DataFrame.
Supplementary view (GET /view) → read cache → pandas derive kpi/trend/heatmap/comparison/detail.

Cache layers:
  L1: ProcessLevelCache (in-process, per-worker)
  L2: Redis (cross-worker, parquet bytes encoded as base64 string)
"""

from __future__ import annotations

import base64
import hashlib
import io
import json
import logging
from functools import lru_cache
from pathlib import Path
from typing import Any, Dict, List, Optional

import pandas as pd

from mes_dashboard.core.cache import ProcessLevelCache, register_process_cache
from mes_dashboard.core.database import read_sql_df_slow as read_sql_df
from mes_dashboard.core.redis_client import (
    REDIS_ENABLED,
    get_key,
    get_redis_client,
)

logger = logging.getLogger("mes_dashboard.resource_dataset_cache")

_CACHE_TTL = 900  # 15 minutes
_CACHE_MAX_SIZE = 8
_REDIS_NAMESPACE = "resource_dataset"

_dataset_cache = ProcessLevelCache(ttl_seconds=_CACHE_TTL, max_size=_CACHE_MAX_SIZE)
register_process_cache(
    "resource_dataset", _dataset_cache, "Resource Dataset (L1, 15min)"
)

_SQL_DIR = Path(__file__).resolve().parent.parent / "sql" / "resource_history"


# ============================================================
# SQL loading
# ============================================================


@lru_cache(maxsize=4)
def _load_sql(name: str) -> str:
    path = _SQL_DIR / f"{name}.sql"
    return path.read_text(encoding="utf-8")


# ============================================================
# Query ID
# ============================================================


def _make_query_id(params: dict) -> str:
    """Deterministic hash from primary query params."""
    canonical = json.dumps(params, sort_keys=True, ensure_ascii=False, default=str)
    return hashlib.sha256(canonical.encode("utf-8")).hexdigest()[:16]


# ============================================================
# Redis L2 helpers (parquet <-> base64 string)
# ============================================================


def _redis_key(query_id: str) -> str:
    return get_key(f"{_REDIS_NAMESPACE}:{query_id}")


def _redis_store_df(query_id: str, df: pd.DataFrame) -> None:
    if not REDIS_ENABLED:
        return
    client = get_redis_client()
    if client is None:
        return
    try:
        buf = io.BytesIO()
        df.to_parquet(buf, engine="pyarrow", index=False)
        encoded = base64.b64encode(buf.getvalue()).decode("ascii")
        client.setex(_redis_key(query_id), _CACHE_TTL, encoded)
    except Exception as exc:
        logger.warning("Failed to store DataFrame in Redis: %s", exc)


def _redis_load_df(query_id: str) -> Optional[pd.DataFrame]:
    if not REDIS_ENABLED:
        return None
    client = get_redis_client()
    if client is None:
        return None
    try:
        encoded = client.get(_redis_key(query_id))
        if encoded is None:
            return None
        raw = base64.b64decode(encoded)
        return pd.read_parquet(io.BytesIO(raw), engine="pyarrow")
    except Exception as exc:
        logger.warning("Failed to load DataFrame from Redis: %s", exc)
        return None


# ============================================================
# Cache read (L1 -> L2 -> None)
# ============================================================


def _get_cached_df(query_id: str) -> Optional[pd.DataFrame]:
    df = _dataset_cache.get(query_id)
    if df is not None:
        return df
    df = _redis_load_df(query_id)
    if df is not None:
        _dataset_cache.set(query_id, df)
    return df


def _store_df(query_id: str, df: pd.DataFrame) -> None:
    _dataset_cache.set(query_id, df)
    _redis_store_df(query_id, df)


# ============================================================
# Resource cache integration (reuse existing service)
# ============================================================


def _get_filtered_resources_and_lookup(
    *,
    workcenter_groups: Optional[List[str]] = None,
    families: Optional[List[str]] = None,
    resource_ids: Optional[List[str]] = None,
    is_production: bool = False,
    is_key: bool = False,
    is_monitor: bool = False,
) -> tuple:
    """Returns (resources_list, resource_lookup_dict, historyid_filter_sql)."""
    from mes_dashboard.services.resource_history_service import (
        _get_filtered_resources,
        _build_resource_lookup,
        _build_historyid_filter,
    )

    resources = _get_filtered_resources(
        workcenter_groups=workcenter_groups,
        families=families,
        resource_ids=resource_ids,
        is_production=is_production,
        is_key=is_key,
        is_monitor=is_monitor,
    )
    lookup = _build_resource_lookup(resources)
    historyid_filter = _build_historyid_filter(resources)
    return resources, lookup, historyid_filter


def _get_resource_lookup() -> Dict[str, Dict[str, Any]]:
    """Get current resource lookup from cache (for view-time dimension merge)."""
    from mes_dashboard.services.resource_history_service import (
        _get_filtered_resources,
        _build_resource_lookup,
    )

    resources = _get_filtered_resources()
    return _build_resource_lookup(resources)


def _get_workcenter_mapping() -> Dict[str, Dict[str, Any]]:
    from mes_dashboard.services.filter_cache import get_workcenter_mapping

    return get_workcenter_mapping() or {}


# ============================================================
# Primary query
# ============================================================


def execute_primary_query(
    *,
    start_date: str,
    end_date: str,
    granularity: str = "day",
    workcenter_groups: Optional[List[str]] = None,
    families: Optional[List[str]] = None,
    resource_ids: Optional[List[str]] = None,
    is_production: bool = False,
    is_key: bool = False,
    is_monitor: bool = False,
) -> Dict[str, Any]:
    """Execute single Oracle query -> cache DataFrame -> return structured result."""

    query_id_input = {
        "start_date": start_date,
        "end_date": end_date,
        "workcenter_groups": sorted(workcenter_groups or []),
        "families": sorted(families or []),
        "resource_ids": sorted(resource_ids or []),
        "is_production": is_production,
        "is_key": is_key,
        "is_monitor": is_monitor,
    }
    query_id = _make_query_id(query_id_input)

    cached_df = _get_cached_df(query_id)
    if cached_df is not None:
        logger.info("Resource dataset cache hit for query_id=%s", query_id)
    else:
        logger.info(
            "Resource dataset cache miss for query_id=%s, querying Oracle", query_id
        )

        resources, lookup, historyid_filter = _get_filtered_resources_and_lookup(
            workcenter_groups=workcenter_groups,
            families=families,
            resource_ids=resource_ids,
            is_production=is_production,
            is_key=is_key,
            is_monitor=is_monitor,
        )

        if not resources:
            return {
                "query_id": query_id,
                "summary": _empty_summary(),
                "detail": _empty_detail(),
            }

        sql = _load_sql("base_facts")
        sql = sql.replace("{{ HISTORYID_FILTER }}", historyid_filter)
        params = {"start_date": start_date, "end_date": end_date}
        df = read_sql_df(sql, params)
        if df is None:
            df = pd.DataFrame()

        if not df.empty:
            _store_df(query_id, df)

        cached_df = df

    resource_lookup = _get_resource_lookup()
    wc_mapping = _get_workcenter_mapping()

    summary = _derive_summary(cached_df, resource_lookup, wc_mapping, granularity)
    detail = _derive_detail(cached_df, resource_lookup, wc_mapping)

    return {
        "query_id": query_id,
        "summary": summary,
        "detail": detail,
    }


# ============================================================
# View (supplementary — cache only)
# ============================================================


def apply_view(
    *,
    query_id: str,
    granularity: str = "day",
) -> Optional[Dict[str, Any]]:
    """Read cache -> derive views. Returns None if expired."""
    df = _get_cached_df(query_id)
    if df is None:
        return None

    resource_lookup = _get_resource_lookup()
    wc_mapping = _get_workcenter_mapping()

    summary = _derive_summary(df, resource_lookup, wc_mapping, granularity)
    detail = _derive_detail(df, resource_lookup, wc_mapping)

    return {
        "summary": summary,
        "detail": detail,
    }


# ============================================================
# Master derivation
# ============================================================


def _derive_summary(
    df: pd.DataFrame,
    resource_lookup: Dict[str, Dict[str, Any]],
    wc_mapping: Dict[str, Dict[str, Any]],
    granularity: str,
) -> Dict[str, Any]:
    if df is None or df.empty:
        return _empty_summary()

    return {
        "kpi": _derive_kpi(df),
        "trend": _derive_trend(df, granularity),
        "heatmap": _derive_heatmap(df, resource_lookup, wc_mapping, granularity),
        "workcenter_comparison": _derive_comparison(df, resource_lookup, wc_mapping),
    }


def _empty_summary() -> Dict[str, Any]:
    return {
        "kpi": _empty_kpi(),
        "trend": [],
        "heatmap": [],
        "workcenter_comparison": [],
    }


def _empty_detail() -> Dict[str, Any]:
    return {"data": [], "total": 0, "truncated": False, "max_records": None}


def _empty_kpi() -> Dict[str, Any]:
    return {
        "ou_pct": 0,
        "availability_pct": 0,
        "prd_hours": 0,
        "prd_pct": 0,
        "sby_hours": 0,
        "sby_pct": 0,
        "udt_hours": 0,
        "udt_pct": 0,
        "sdt_hours": 0,
        "sdt_pct": 0,
        "egt_hours": 0,
        "egt_pct": 0,
        "nst_hours": 0,
        "nst_pct": 0,
        "machine_count": 0,
    }


# ============================================================
# Helpers (reuse existing formulas)
# ============================================================


def _sf(value, default=0.0) -> float:
    """Safe float."""
    if value is None or pd.isna(value):
        return default
    return float(value)


def _calc_ou_pct(prd, sby, udt, sdt, egt) -> float:
    denom = prd + sby + udt + sdt + egt
    return round(prd / denom * 100, 1) if denom > 0 else 0


def _calc_avail_pct(prd, sby, udt, sdt, egt, nst) -> float:
    num = prd + sby + egt
    denom = prd + sby + egt + sdt + udt + nst
    return round(num / denom * 100, 1) if denom > 0 else 0


def _status_pct(val, total) -> float:
    return round(val / total * 100, 1) if total > 0 else 0


def _trunc_date(dt, granularity: str) -> str:
    """Truncate a date value to the given granularity period string."""
    if pd.isna(dt):
        return ""
    ts = pd.Timestamp(dt)
    if granularity == "year":
        return ts.strftime("%Y")
    if granularity == "month":
        return ts.strftime("%Y-%m")
    if granularity == "week":
        return (ts - pd.Timedelta(days=ts.weekday())).strftime("%Y-%m-%d")
    return ts.strftime("%Y-%m-%d")


# ============================================================
# Derivation: KPI
# ============================================================


def _derive_kpi(df: pd.DataFrame) -> Dict[str, Any]:
    if df is None or df.empty:
        return _empty_kpi()

    prd = _sf(df["PRD_HOURS"].sum())
    sby = _sf(df["SBY_HOURS"].sum())
    udt = _sf(df["UDT_HOURS"].sum())
    sdt = _sf(df["SDT_HOURS"].sum())
    egt = _sf(df["EGT_HOURS"].sum())
    nst = _sf(df["NST_HOURS"].sum())
    total = prd + sby + udt + sdt + egt + nst
    machine_count = int(df["HISTORYID"].nunique())

    return {
        "ou_pct": _calc_ou_pct(prd, sby, udt, sdt, egt),
        "availability_pct": _calc_avail_pct(prd, sby, udt, sdt, egt, nst),
        "prd_hours": round(prd, 1),
        "prd_pct": _status_pct(prd, total),
        "sby_hours": round(sby, 1),
        "sby_pct": _status_pct(sby, total),
        "udt_hours": round(udt, 1),
        "udt_pct": _status_pct(udt, total),
        "sdt_hours": round(sdt, 1),
        "sdt_pct": _status_pct(sdt, total),
        "egt_hours": round(egt, 1),
        "egt_pct": _status_pct(egt, total),
        "nst_hours": round(nst, 1),
        "nst_pct": _status_pct(nst, total),
        "machine_count": machine_count,
    }


# ============================================================
# Derivation: Trend
# ============================================================


def _derive_trend(df: pd.DataFrame, granularity: str) -> List[Dict[str, Any]]:
    if df is None or df.empty:
        return []

    df = df.copy()
    df["_period"] = df["DATA_DATE"].apply(lambda d: _trunc_date(d, granularity))

    grouped = (
        df.groupby("_period", sort=True)
        .agg(
            PRD_HOURS=("PRD_HOURS", "sum"),
            SBY_HOURS=("SBY_HOURS", "sum"),
            UDT_HOURS=("UDT_HOURS", "sum"),
            SDT_HOURS=("SDT_HOURS", "sum"),
            EGT_HOURS=("EGT_HOURS", "sum"),
            NST_HOURS=("NST_HOURS", "sum"),
        )
        .reset_index()
    )

    items: List[Dict[str, Any]] = []
    for _, row in grouped.iterrows():
        prd = _sf(row["PRD_HOURS"])
        sby = _sf(row["SBY_HOURS"])
        udt = _sf(row["UDT_HOURS"])
        sdt = _sf(row["SDT_HOURS"])
        egt = _sf(row["EGT_HOURS"])
        nst = _sf(row["NST_HOURS"])
        items.append(
            {
                "date": row["_period"],
                "ou_pct": _calc_ou_pct(prd, sby, udt, sdt, egt),
                "availability_pct": _calc_avail_pct(prd, sby, udt, sdt, egt, nst),
                "prd_hours": round(prd, 1),
                "sby_hours": round(sby, 1),
                "udt_hours": round(udt, 1),
                "sdt_hours": round(sdt, 1),
                "egt_hours": round(egt, 1),
                "nst_hours": round(nst, 1),
            }
        )

    return items


# ============================================================
# Derivation: Heatmap
# ============================================================


def _derive_heatmap(
    df: pd.DataFrame,
    resource_lookup: Dict[str, Dict[str, Any]],
    wc_mapping: Dict[str, Dict[str, Any]],
    granularity: str,
) -> List[Dict[str, Any]]:
    if df is None or df.empty:
        return []

    rows: List[Dict[str, Any]] = []
    wc_seq_map: Dict[str, int] = {}

    for _, row in df.iterrows():
        historyid = row["HISTORYID"]
        resource_info = resource_lookup.get(historyid, {})
        if not resource_info:
            continue

        wc_name = resource_info.get("WORKCENTERNAME", "")
        if not wc_name:
            continue

        wc_info = wc_mapping.get(wc_name, {})
        wc_group = wc_info.get("group", wc_name)
        wc_seq = wc_info.get("sequence", 999)
        wc_seq_map[wc_group] = wc_seq
        date_str = _trunc_date(row["DATA_DATE"], granularity)

        rows.append(
            {
                "wc": wc_group,
                "date": date_str,
                "prd": _sf(row["PRD_HOURS"]),
                "sby": _sf(row["SBY_HOURS"]),
                "udt": _sf(row["UDT_HOURS"]),
                "sdt": _sf(row["SDT_HOURS"]),
                "egt": _sf(row["EGT_HOURS"]),
            }
        )

    if not rows:
        return []

    tmp = pd.DataFrame(rows)
    agg = (
        tmp.groupby(["wc", "date"], sort=False)
        .agg(prd=("prd", "sum"), sby=("sby", "sum"), udt=("udt", "sum"), sdt=("sdt", "sum"), egt=("egt", "sum"))
        .reset_index()
    )

    items: List[Dict[str, Any]] = []
    for _, r in agg.iterrows():
        items.append(
            {
                "workcenter": r["wc"],
                "workcenter_seq": wc_seq_map.get(r["wc"], 999),
                "date": r["date"],
                "ou_pct": _calc_ou_pct(r["prd"], r["sby"], r["udt"], r["sdt"], r["egt"]),
            }
        )

    items.sort(key=lambda x: (x["workcenter_seq"], x["date"] or ""))
    return items


# ============================================================
# Derivation: Workcenter Comparison
# ============================================================


def _derive_comparison(
    df: pd.DataFrame,
    resource_lookup: Dict[str, Dict[str, Any]],
    wc_mapping: Dict[str, Dict[str, Any]],
) -> List[Dict[str, Any]]:
    if df is None or df.empty:
        return []

    # Aggregate by HISTORYID first
    by_resource = (
        df.groupby("HISTORYID", sort=False)
        .agg(
            prd=("PRD_HOURS", "sum"),
            sby=("SBY_HOURS", "sum"),
            udt=("UDT_HOURS", "sum"),
            sdt=("SDT_HOURS", "sum"),
            egt=("EGT_HOURS", "sum"),
        )
        .reset_index()
    )

    # Then aggregate by workcenter group
    agg: Dict[str, Dict[str, float]] = {}
    for _, row in by_resource.iterrows():
        historyid = row["HISTORYID"]
        resource_info = resource_lookup.get(historyid, {})
        if not resource_info:
            continue

        wc_name = resource_info.get("WORKCENTERNAME", "")
        if not wc_name:
            continue

        wc_info = wc_mapping.get(wc_name, {})
        wc_group = wc_info.get("group", wc_name)

        if wc_group not in agg:
            agg[wc_group] = {"prd": 0, "sby": 0, "udt": 0, "sdt": 0, "egt": 0, "mc": 0}

        agg[wc_group]["prd"] += _sf(row["prd"])
        agg[wc_group]["sby"] += _sf(row["sby"])
        agg[wc_group]["udt"] += _sf(row["udt"])
        agg[wc_group]["sdt"] += _sf(row["sdt"])
        agg[wc_group]["egt"] += _sf(row["egt"])
        agg[wc_group]["mc"] += 1

    items = [
        {
            "workcenter": wc,
            "ou_pct": _calc_ou_pct(d["prd"], d["sby"], d["udt"], d["sdt"], d["egt"]),
            "prd_hours": round(d["prd"], 1),
            "machine_count": d["mc"],
        }
        for wc, d in agg.items()
    ]
    items.sort(key=lambda x: x["ou_pct"], reverse=True)
    return items


# ============================================================
# Derivation: Detail
# ============================================================


def _derive_detail(
    df: pd.DataFrame,
    resource_lookup: Dict[str, Dict[str, Any]],
    wc_mapping: Dict[str, Dict[str, Any]],
) -> Dict[str, Any]:
    if df is None or df.empty:
        return _empty_detail()

    by_resource = (
        df.groupby("HISTORYID", sort=False)
        .agg(
            PRD_HOURS=("PRD_HOURS", "sum"),
            SBY_HOURS=("SBY_HOURS", "sum"),
            UDT_HOURS=("UDT_HOURS", "sum"),
            SDT_HOURS=("SDT_HOURS", "sum"),
            EGT_HOURS=("EGT_HOURS", "sum"),
            NST_HOURS=("NST_HOURS", "sum"),
            TOTAL_HOURS=("TOTAL_HOURS", "sum"),
        )
        .reset_index()
    )

    data: List[Dict[str, Any]] = []
    for _, row in by_resource.iterrows():
        historyid = row["HISTORYID"]
        resource_info = resource_lookup.get(historyid, {})
        if not resource_info:
            continue

        prd = _sf(row["PRD_HOURS"])
        sby = _sf(row["SBY_HOURS"])
        udt = _sf(row["UDT_HOURS"])
        sdt = _sf(row["SDT_HOURS"])
        egt = _sf(row["EGT_HOURS"])
        nst = _sf(row["NST_HOURS"])
        total = _sf(row["TOTAL_HOURS"])

        wc_name = resource_info.get("WORKCENTERNAME", "")
        wc_info = wc_mapping.get(wc_name, {})
        wc_group = wc_info.get("group", wc_name)
        wc_seq = wc_info.get("sequence", 999)
        family = resource_info.get("RESOURCEFAMILYNAME", "")
        resource_name = resource_info.get("RESOURCENAME", "")

        data.append(
            {
                "workcenter": wc_group,
                "workcenter_seq": wc_seq,
                "family": family or "",
                "resource": resource_name or "",
                "ou_pct": _calc_ou_pct(prd, sby, udt, sdt, egt),
                "availability_pct": _calc_avail_pct(prd, sby, udt, sdt, egt, nst),
                "prd_hours": round(prd, 1),
                "prd_pct": _status_pct(prd, total),
                "sby_hours": round(sby, 1),
                "sby_pct": _status_pct(sby, total),
                "udt_hours": round(udt, 1),
                "udt_pct": _status_pct(udt, total),
                "sdt_hours": round(sdt, 1),
                "sdt_pct": _status_pct(sdt, total),
                "egt_hours": round(egt, 1),
                "egt_pct": _status_pct(egt, total),
                "nst_hours": round(nst, 1),
                "nst_pct": _status_pct(nst, total),
                "machine_count": 1,
            }
        )

    data.sort(key=lambda x: (x["workcenter_seq"], x["family"], x["resource"]))

    return {
        "data": data,
        "total": len(data),
        "truncated": False,
        "max_records": None,
    }
