"""Controller methods for MCP tools — all business logic lives here.

Tools call these controllers and return their results directly.
Controllers handle CSV loading, series extraction, provider selection,
and result formatting.
"""

from __future__ import annotations

import csv
import time
from pathlib import Path
from typing import Any, Optional


def read_file(path: str, head: int = 0) -> dict[str, Any]:
    """Read a file and return its contents.

    Supports CSV (returns rows as list of dicts) and plain text.

    Args:
        path: Absolute or relative file path.
        head: If > 0, return only the first N rows/lines.
    """
    p = Path(path)
    if not p.exists():
        return {"status": "error", "error": f"File not found: {path}"}
    if not p.is_file():
        return {"status": "error", "error": f"Not a file: {path}"}

    size = p.stat().st_size
    if size > 50_000_000:  # 50MB guard
        return {"status": "error", "error": f"File too large: {size:,} bytes (max 50MB)"}

    if p.suffix.lower() == ".csv":
        with open(p) as f:
            rows = list(csv.DictReader(f))
        if head > 0:
            rows = rows[:head]
        return {
            "status": "ok",
            "format": "csv",
            "path": str(p),
            "columns": list(rows[0].keys()) if rows else [],
            "row_count": len(rows),
            "rows": rows,
        }

    text = p.read_text()
    lines = text.splitlines()
    if head > 0:
        lines = lines[:head]
        text = "\n".join(lines)

    return {
        "status": "ok",
        "format": "text",
        "path": str(p),
        "line_count": len(lines),
        "content": text,
    }


def _series_std(series: list[float]) -> float:
    """Standard deviation of a series (population)."""
    if len(series) < 2:
        return 0.0
    mean = sum(series) / len(series)
    return (sum((x - mean) ** 2 for x in series) / len(series)) ** 0.5


def forecast(
    data_path: str,
    product_id: Optional[str] = None,
    horizon: int = 14,
    method: str = "auto",
    season_length: int = 7,
    holdout: int = 30,
) -> dict[str, Any]:
    """Run demand forecasting on a CSV time series.

    Loads CSV, extracts product series, runs forecast, returns structured results.

    Args:
        data_path: Path to daily_demand.csv (columns: date, product_id, units_sold).
        product_id: Which product to forecast. If None, picks the highest-volume product.
        horizon: Days ahead to forecast.
        method: Model — auto, moving_average, exponential_smoothing, croston, arima, ets, theta.
        season_length: Seasonal period (default 7 for weekly).
        holdout: Days to hold out for accuracy evaluation.
    """
    from platoon.learning.providers import get_provider

    # Validate horizon
    if horizon < 1 and holdout < 1:
        return {"status": "error", "error": "horizon must be >= 1 (nothing to forecast)"}

    # Load data
    p = Path(data_path)
    if not p.exists():
        return {"status": "error", "error": f"File not found: {data_path}"}

    with open(p) as f:
        rows = list(csv.DictReader(f))

    if not rows:
        return {"status": "error", "error": "CSV is empty"}

    required = {"date", "product_id", "units_sold"}
    actual = set(rows[0].keys())
    if not required.issubset(actual):
        return {"status": "error", "error": f"CSV must have columns: {required}. Found: {actual}"}

    # Resolve product
    if not product_id:
        totals: dict[str, float] = {}
        for r in rows:
            totals[r["product_id"]] = totals.get(r["product_id"], 0) + float(r["units_sold"])
        product_id = max(totals, key=totals.get)

    # Extract series
    filtered = [r for r in rows if r["product_id"] == product_id]
    if not filtered:
        available = sorted(set(r["product_id"] for r in rows))[:10]
        return {"status": "error", "error": f"No data for '{product_id}'", "available": available}

    dates = [r["date"] for r in filtered]
    series = [float(r["units_sold"]) for r in filtered]

    # Split train/holdout
    if 0 < holdout < len(series):
        train = series[:-holdout]
        actual_values = series[-holdout:]
    else:
        train = series
        actual_values = None
        holdout = 0

    # Pick backend
    sf_methods = {"arima", "ets", "theta", "seasonal_naive", "auto"}
    builtin_methods = {"moving_average", "exponential_smoothing", "croston", "seasonal_decompose"}

    if method in sf_methods:
        backend = "statsforecast"
    elif method in builtin_methods:
        backend = "builtin"
    else:
        backend = "statsforecast"

    try:
        provider = get_provider("forecasting", backend=backend)
    except (ValueError, ImportError) as e:
        # Fall back to builtin
        provider = get_provider("forecasting", backend="builtin")
        if method in sf_methods:
            method = "exponential_smoothing"

    # Run forecast
    forecast_horizon = holdout if actual_values else horizon
    # Only pass season_length to methods that use it
    kwargs = {}
    if method not in ("moving_average", "exponential_smoothing", "croston"):
        kwargs["season_length"] = season_length

    t0 = time.time()
    try:
        result = provider.forecast(train, horizon=forecast_horizon, method=method, **kwargs)
    except Exception as e:
        return {"status": "error", "error": f"Forecast failed: {e}"}
    elapsed_ms = (time.time() - t0) * 1000

    # Compute accuracy
    mae = None
    if actual_values:
        n = min(len(actual_values), len(result.values))
        mae = sum(abs(a - p) for a, p in zip(actual_values[:n], result.values[:n])) / n

    zero_pct = sum(1 for v in series if v == 0) / len(series) * 100

    # Use the provider's reported method when it includes backend info,
    # otherwise prepend the backend name for clarity
    if result.method and ":" in result.method:
        reported_method = result.method
    else:
        reported_method = f"{backend}:{method}"

    # Post-process confidence intervals
    lower = result.lower_bound
    upper = result.upper_bound

    # Generate synthetic CIs when the model doesn't provide them (e.g. Croston)
    if not lower or not upper:
        std = _series_std(train)
        lower = [round(max(0, v - 1.96 * std), 4) for v in result.values]
        upper = [round(v + 1.96 * std, 4) for v in result.values]

    # Clamp lower bounds to 0 — demand can't be negative
    lower = [round(max(0, v), 4) for v in lower]

    # Detect trend: compare first half vs second half of forecast
    fvals = result.values[:forecast_horizon]
    trend = None
    if len(fvals) >= 2:
        mid = len(fvals) // 2
        first_half = sum(fvals[:mid]) / mid if mid else 0
        second_half = sum(fvals[mid:]) / (len(fvals) - mid) if (len(fvals) - mid) else 0
        if first_half > 0:
            pct = (second_half - first_half) / first_half * 100
            if pct > 5:
                trend = "up"
            elif pct < -5:
                trend = "down"
            else:
                trend = "stable"

    # Detect seasonality: check if forecast repeats with period = season_length
    seasonality_detected = False
    if len(fvals) >= season_length * 2:
        cycle1 = fvals[:season_length]
        cycle2 = fvals[season_length:season_length * 2]
        if cycle1 and cycle2:
            diffs = [abs(a - b) for a, b in zip(cycle1, cycle2)]
            max_val = max(max(cycle1), max(cycle2), 1)
            if sum(diffs) / len(diffs) / max_val < 0.05:  # <5% deviation = repeating
                seasonality_detected = True

    # Warn about insufficient data
    warning = None
    if len(train) < 7:
        warning = f"Very short series ({len(train)} points) — forecast may be unreliable"

    out = {
        "status": "ok",
        "product_id": product_id,
        "method": reported_method,
        "series_length": len(series),
        "zero_pct": round(zero_pct, 1),
        "train_length": len(train),
        "holdout_length": holdout,
        "horizon": forecast_horizon,
        "forecast": result.values,
        "lower_bound": lower,
        "upper_bound": upper,
        "predicted_sum": round(sum(fvals), 1),
        "predicted_mean": round(sum(fvals) / len(fvals), 2) if fvals else 0,
        "trend": trend,
        "seasonality_detected": seasonality_detected,
        "mae": round(mae, 2) if mae is not None else None,
        "elapsed_ms": round(elapsed_ms, 1),
    }
    if warning:
        out["warning"] = warning
    return out


def optimize(
    data_path: str,
    product_id: Optional[str] = None,
    orders_path: Optional[str] = None,
    inventory_path: Optional[str] = None,
    service_level: float = 0.95,
) -> dict[str, Any]:
    """Run inventory optimization on CSV data.

    Computes EOQ, safety stock, reorder point, ABC classification,
    and stockout risk for products.

    Args:
        data_path: Path to products.csv (columns: product_id, sku, cost, price, base_daily_demand, lead_time_days).
        product_id: Analyze a single product. If None, analyzes all.
        orders_path: Optional orders.csv for ABC classification.
        inventory_path: Optional inventory.csv for current stock levels.
        service_level: Target service level for safety stock (0-1, default 0.95).
    """
    from platoon.learning.providers import get_provider

    # Validate service_level
    if not (0 < service_level < 1):
        return {"status": "error", "error": f"service_level must be between 0 and 1 exclusive, got {service_level}"}

    p = Path(data_path)
    if not p.exists():
        return {"status": "error", "error": f"File not found: {data_path}"}

    with open(p) as f:
        products = list(csv.DictReader(f))

    if not products:
        return {"status": "error", "error": "Products CSV is empty"}

    inv = get_provider("inventory")

    # Filter to single product if specified
    if product_id:
        products = [r for r in products if r["product_id"] == product_id]
        if not products:
            return {"status": "error", "error": f"Product '{product_id}' not found"}

    # ABC classification from orders
    abc_lookup: dict[str, str] = {}
    abc_summary = None
    if orders_path:
        op = Path(orders_path)
        if op.exists():
            with open(op) as f:
                orders = list(csv.DictReader(f))
            revenue_by_product: dict[str, float] = {}
            for row in orders:
                pid = row["product_id"]
                revenue_by_product[pid] = revenue_by_product.get(pid, 0) + float(row["line_total"])
            items = [{"sku": k, "revenue": v} for k, v in revenue_by_product.items()]
            abc = inv.abc(items)
            for cls_name, cls_items in abc.items():
                for item in cls_items:
                    abc_lookup[item["sku"]] = cls_name
            abc_summary = {c: len(items) for c, items in abc.items()}

    # Current stock levels
    latest_stock: dict[str, int] = {}
    if inventory_path:
        ip = Path(inventory_path)
        if ip.exists():
            with open(ip) as f:
                for row in csv.DictReader(f):
                    latest_stock[row["product_id"]] = int(row["closing_stock"])

    # Per-product optimization
    results = []
    alerts = []
    for row in products:
        pid = row["product_id"]
        daily_demand = float(row["base_daily_demand"])
        annual_demand = daily_demand * 365
        cost = float(row["cost"])
        lead_time = int(row["lead_time_days"])
        demand_std = daily_demand * 0.3
        holding_cost = cost * 0.25
        ordering_cost = 50.0
        stock = latest_stock.get(pid, int(row.get("initial_stock", 0)))

        report = inv.full_report(
            sku=row["sku"],
            annual_demand=annual_demand,
            demand_std=demand_std,
            ordering_cost=ordering_cost,
            holding_cost=holding_cost,
            lead_time_days=lead_time,
            current_stock=stock,
            service_level=service_level,
        )

        price = float(row.get("price", 0))
        days_of_stock = round(stock / daily_demand, 1) if daily_demand > 0 else None
        restock_cost = round(report.eoq.order_quantity * cost, 2)
        daily_revenue = round(daily_demand * price, 2)

        entry = {
            "product_id": pid,
            "sku": report.sku,
            "abc_class": abc_lookup.get(pid, "—"),
            "price": price,
            "cost": cost,
            "daily_demand": round(daily_demand, 2),
            "lead_time_days": lead_time,
            "eoq": round(report.eoq.order_quantity),
            "restock_cost": restock_cost,
            "safety_stock": round(report.safety_stock),
            "reorder_point": round(report.reorder_point),
            "current_stock": stock,
            "days_of_stock": days_of_stock,
            "daily_revenue": daily_revenue,
            "stockout_risk": round(report.stockout_risk, 3) if report.stockout_risk is not None else None,
        }
        results.append(entry)

        if report.stockout_risk and report.stockout_risk > 0.3:
            alerts.append({
                "product_id": pid,
                "sku": report.sku,
                "risk": round(report.stockout_risk, 3),
                "reorder_point": round(report.reorder_point),
                "current_stock": stock,
                "days_of_stock": days_of_stock,
                "daily_revenue": daily_revenue,
                "restock_cost": restock_cost,
                "action": f"Reorder {round(report.eoq.order_quantity)} units",
            })

    return {
        "status": "ok",
        "product_count": len(results),
        "abc_summary": abc_summary,
        "results": results,
        "alerts": sorted(alerts, key=lambda a: a["risk"], reverse=True),
    }
