"""
Feature vector builder.

Converts (district, crop, month) into the exact numeric feature vectors
each model was trained on. This is the single source of truth for
feature order and meaning — if you change this, you MUST retrain every
model that consumes it, because XGBoost/RandomForest/DecisionTree have
no concept of column names at inference time, only column position.

The historical climate stats CSV referenced here is produced by the
ml_pipeline/feature_engineering scripts and must be present in
data/processed/ before the API can serve real (non-fallback) features.
"""

from pathlib import Path

import numpy as np
import pandas as pd

from app.core.logging_config import get_logger
from app.services.reference_data import reference_data

logger = get_logger(__name__)

CLIMATE_HISTORY_PATH = Path("data/processed/climate_clean.csv")

# Maps calendar month -> season index used consistently across all models.
# kharif=0 (monsoon), rabi=1 (winter), summer=2
SEASON_MAP = {
    6: 0, 7: 0, 8: 0, 9: 0,
    10: 1, 11: 1, 12: 1, 1: 1,
    2: 2, 3: 2, 4: 2, 5: 2,
}

# Feature order MUST match the order used in ml_pipeline/training/*.py
# Any change here requires retraining every model. Do not reorder casually.
FEATURE_COLUMNS = [
    "rainfall_mm", "temp_c", "humidity",
    "season_index", "rainfall_anomaly",
    "rainfall_ma3", "rainfall_lag1",
    "water_req_mm", "min_temp_c", "max_temp_c",
    "drought_tolerant", "growth_days",
]


class FeatureBuilder:
    def __init__(self) -> None:
        self._climate_history: pd.DataFrame | None = None
        self._loaded = False

    def load(self) -> None:
        if not CLIMATE_HISTORY_PATH.exists():
            logger.warning(
                f"{CLIMATE_HISTORY_PATH} not found — feature builder will use "
                f"fallback climate estimates. Real predictions require this file "
                f"to be generated by the ml_pipeline before going to production."
            )
            self._climate_history = pd.DataFrame(
                columns=["district", "month", "rainfall_mm", "temp_c", "humidity"]
            )
        else:
            self._climate_history = pd.read_csv(CLIMATE_HISTORY_PATH)

        self._loaded = True

    def _ensure_loaded(self) -> None:
        if not self._loaded:
            raise RuntimeError(
                "FeatureBuilder used before load() was called. "
                "This indicates a startup-order bug."
            )

    def _historical_climate(self, district: str, month: int) -> dict:
        """Returns mean rainfall/temp/humidity for this district+month across history."""
        hist = self._climate_history[
            (self._climate_history["district"] == district)
            & (self._climate_history["month"] == month)
        ]
        if hist.empty:
            # Fallback estimate — clearly logged so it's never mistaken for real data.
            logger.warning(
                f"No historical climate data for district='{district}' month={month}, "
                f"using generic fallback values"
            )
            return {"rainfall_mm": 100.0, "temp_c": 30.0, "humidity": 70.0}
        return {
            "rainfall_mm": float(hist["rainfall_mm"].mean()),
            "temp_c": float(hist["temp_c"].mean()) if "temp_c" in hist else 30.0,
            "humidity": float(hist["humidity"].mean()) if "humidity" in hist else 70.0,
        }

    def build(self, district: str, crop: str, month: int) -> np.ndarray:
        """
        Builds the feature vector for a single (district, crop, month) query.
        Raises UnsupportedCropError / UnsupportedDistrictError via
        reference_data if either input is invalid — callers should let
        that propagate, not catch it here.
        """
        self._ensure_loaded()

        crop_props = reference_data.get_crop(crop)
        reference_data.get_district(district)  # validates; lat/lon reserved for future grid lookup

        current = self._historical_climate(district, month)

        prev_month = month - 1 if month > 1 else 12
        prev = self._historical_climate(district, prev_month)

        rainfall_lag1 = prev["rainfall_mm"]
        rainfall_ma3 = (current["rainfall_mm"] + rainfall_lag1) / 2

        # Anomaly is properly computed from the SARIMAX forecast at inference
        # time (see app/services/aggregator.py) — 0.0 here is a neutral
        # placeholder for the static feature vector used by the classifier/
        # regressor/tree models, which were trained with anomaly as one
        # input among several, not as their sole driver.
        rainfall_anomaly = 0.0

        season_index = SEASON_MAP.get(month, 0)

        vector = np.array([
            current["rainfall_mm"],
            current["temp_c"],
            current["humidity"],
            season_index,
            rainfall_anomaly,
            rainfall_ma3,
            rainfall_lag1,
            crop_props["water_req_mm"],
            crop_props["min_temp_c"],
            crop_props["max_temp_c"],
            float(crop_props["drought_tolerant"]),
            crop_props["growth_days"],
        ], dtype=np.float64)

        if vector.shape[0] != len(FEATURE_COLUMNS):
            # Defensive check — catches a future bug where someone adds a
            # field above without updating FEATURE_COLUMNS, which would
            # silently misalign every downstream model's inputs.
            raise RuntimeError(
                f"Feature vector length {vector.shape[0]} does not match "
                f"expected {len(FEATURE_COLUMNS)} columns"
            )

        return vector


# Single shared instance — loaded once during app startup
feature_builder = FeatureBuilder()
