"""CLI for learning models: forecast and optimize.

Usage:
    platoon forecast --data data/samples/daily_demand.csv --product PROD-1001 --horizon 14
    platoon forecast --data data/samples/daily_demand.csv --product PROD-1001 --method arima --backend statsforecast
    platoon forecast --data data/samples/daily_demand.csv --category Electronics --horizon 30
    platoon forecast --data data/samples/daily_demand.csv --top 5 --compare

    platoon optimize --data data/samples/products.csv --orders data/samples/orders.csv
    platoon optimize --data data/samples/products.csv --product PROD-1001 --inventory data/samples/inventory.csv
"""

from __future__ import annotations

import csv
import sys
import time
from pathlib import Path
from typing import Optional


def _load_csv(path: str) -> list[dict]:
    """Load CSV, bail with clear message if missing."""
    p = Path(path)
    if not p.exists():
        print(f"Error: file not found: {path}", file=sys.stderr)
        print(f"  Generate sample data with: uv run python data/samples/generate.py", file=sys.stderr)
        sys.exit(1)
    with open(p) as f:
        return list(csv.DictReader(f))


def _extract_series(demand_rows: list[dict], product_id: str) -> tuple[list[str], list[float]]:
    """Extract (dates, values) for a single product from daily_demand.csv."""
    filtered = [r for r in demand_rows if r["product_id"] == product_id]
    if not filtered:
        print(f"Error: no data for product '{product_id}'", file=sys.stderr)
        available = sorted(set(r["product_id"] for r in demand_rows))[:10]
        print(f"  Available: {', '.join(available)} ...", file=sys.stderr)
        sys.exit(1)
    dates = [r["date"] for r in filtered]
    values = [float(r["units_sold"]) for r in filtered]
    return dates, values


def _print_forecast_table(results: list[dict]):
    """Print forecast results as a formatted table."""
    # Header
    print(f"\n{'Method':<28} {'Sum':>8} {'Mean/d':>8} {'MAE':>8} {'Time':>8}")
    print("-" * 68)
    for r in results:
        mae_str = f"{r['mae']:.2f}" if r.get("mae") is not None else "—"
        print(f"{r['method']:<28} {r['pred_sum']:>8.0f} {r['pred_mean']:>8.1f} {mae_str:>8} {r['time_ms']:>6.0f}ms")

    # Winner
    with_mae = [r for r in results if r.get("mae") is not None]
    if with_mae:
        best = min(with_mae, key=lambda r: r["mae"])
        print(f"\n  Best by MAE: {best['method']} ({best['mae']:.2f})")


def _print_forecast_detail(result, dates_future: list[str] = None):
    """Print day-by-day forecast with confidence intervals."""
    n = min(len(result.values), 14)  # show up to 14 days
    has_ci = bool(result.lower_bound)

    if has_ci:
        print(f"\n  {'Day':<6} {'Forecast':>10} {'Lo 95%':>10} {'Hi 95%':>10}")
        print("  " + "-" * 38)
        for i in range(n):
            day = dates_future[i] if dates_future else f"d+{i+1}"
            print(f"  {day:<6} {result.values[i]:>10.1f} {result.lower_bound[i]:>10.1f} {result.upper_bound[i]:>10.1f}")
    else:
        print(f"\n  {'Day':<6} {'Forecast':>10}")
        print("  " + "-" * 18)
        for i in range(n):
            day = dates_future[i] if dates_future else f"d+{i+1}"
            print(f"  {day:<6} {result.values[i]:>10.1f}")

    if n < len(result.values):
        print(f"  ... ({len(result.values) - n} more days)")


def run_forecast(args):
    """Run demand forecasting on CSV data."""
    from platoon.learning.providers import get_provider, list_providers

    demand_rows = _load_csv(args.data)

    # Determine which products to forecast
    if args.product:
        product_ids = [args.product]
    elif args.category:
        product_ids = sorted(set(r["product_id"] for r in demand_rows if r.get("category") == args.category))
        if not product_ids:
            print(f"Error: no products in category '{args.category}'", file=sys.stderr)
            sys.exit(1)
    elif args.top:
        # Top N products by total demand
        totals = {}
        for r in demand_rows:
            totals[r["product_id"]] = totals.get(r["product_id"], 0) + float(r["units_sold"])
        product_ids = sorted(totals, key=totals.get, reverse=True)[:args.top]
    else:
        # Default: first product
        product_ids = [demand_rows[0]["product_id"]]

    horizon = args.horizon
    holdout = args.holdout

    # Build method list
    if args.compare:
        methods = [
            ("builtin", "moving_average", {"window": 28}),
            ("builtin", "exponential_smoothing", {"alpha": 0.15}),
            ("statsforecast", "arima", {"season_length": 7}),
            ("statsforecast", "ets", {"season_length": 7}),
            ("statsforecast", "theta", {"season_length": 7}),
        ]
        # Add croston if data looks intermittent
    elif args.method and args.backend:
        methods = [(args.backend, args.method, {"season_length": args.season_length})]
    elif args.method:
        # Auto-pick backend
        sf_methods = {"arima", "ets", "theta", "seasonal_naive", "mstl"}
        backend = "statsforecast" if args.method in sf_methods else "builtin"
        methods = [(backend, args.method, {"season_length": args.season_length})]
    else:
        methods = [("statsforecast", "auto", {"season_length": args.season_length})]

    for pid in product_ids:
        dates, series = _extract_series(demand_rows, pid)
        title = next((r.get("product_title", r.get("title", pid))
                       for r in demand_rows if r["product_id"] == pid), pid)
        # Try to get title from products context in the data
        cat = next((r.get("category", "") for r in demand_rows if r["product_id"] == pid), "")

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

        print(f"\n{'='*68}")
        print(f"  {pid} | {cat}")
        print(f"  {len(series)} days | mean={sum(series)/len(series):.1f}/day | zeros={zero_pct:.0f}%")
        print(f"{'='*68}")

        if holdout > 0 and holdout < len(series):
            train = series[:-holdout]
            actual = series[-holdout:]
            print(f"  Train: {len(train)} days | Holdout: {holdout} days | Forecast: {horizon} days")
        else:
            train = series
            actual = None
            holdout = 0
            print(f"  Full series: {len(train)} days | Forecast: {horizon} days")

        # Auto-add croston for intermittent series in compare mode
        actual_methods = list(methods)
        if args.compare and zero_pct > 40:
            actual_methods.append(("builtin", "croston", {"alpha": 0.1}))
            actual_methods.append(("statsforecast", "croston", {}))

        results = []
        best_result = None

        for backend, method, kwargs in actual_methods:
            try:
                provider = get_provider("forecasting", backend=backend)
            except (ValueError, ImportError) as e:
                print(f"  Skipping {backend}:{method} — {e}", file=sys.stderr)
                continue

            try:
                t0 = time.time()
                forecast_horizon = holdout if (holdout > 0 and actual is not None) else horizon
                result = provider.forecast(train, horizon=forecast_horizon, method=method, **kwargs)
                elapsed = (time.time() - t0) * 1000

                pred_sum = sum(result.values[:forecast_horizon])
                pred_mean = pred_sum / forecast_horizon

                mae = None
                if actual is not None:
                    n = min(len(actual), len(result.values))
                    mae = sum(abs(a - p) for a, p in zip(actual[:n], result.values[:n])) / n

                label = f"{backend}:{method}"
                entry = {
                    "method": label,
                    "pred_sum": pred_sum,
                    "pred_mean": pred_mean,
                    "mae": mae,
                    "time_ms": elapsed,
                    "result": result,
                }
                results.append(entry)

                if best_result is None or (mae is not None and (best_result["mae"] is None or mae < best_result["mae"])):
                    best_result = entry

            except Exception as e:
                print(f"  Error {backend}:{method}: {e}", file=sys.stderr)

        if actual is not None:
            print(f"  Actual holdout: sum={sum(actual):.0f}, mean={sum(actual)/len(actual):.1f}/day")

        _print_forecast_table(results)

        # Show detail for best or single method
        detail = best_result if args.compare and best_result else (results[0] if results else None)
        if detail and not args.quiet:
            print(f"\n  Detail: {detail['method']}")
            _print_forecast_detail(detail["result"])

    if args.output:
        _export_forecast(args.output, product_ids, demand_rows, results)


def _export_forecast(path: str, product_ids, demand_rows, results):
    """Export forecast results to JSON."""
    import json
    out = []
    for r in results:
        out.append({
            "method": r["method"],
            "forecast": r["result"].values,
            "lower_bound": r["result"].lower_bound,
            "upper_bound": r["result"].upper_bound,
            "mae": r["mae"],
            "pred_sum": r["pred_sum"],
        })
    Path(path).write_text(json.dumps(out, indent=2))
    print(f"\n  Exported to {path}")


def run_optimize(args):
    """Run inventory optimization on CSV data."""
    from platoon.learning.providers import get_provider

    products = _load_csv(args.data)
    inv = get_provider("inventory", backend=args.backend or None)

    # Load orders for ABC if provided
    orders = _load_csv(args.orders) if args.orders else None

    # Load inventory for stockout risk if provided
    inventory = _load_csv(args.inventory) if args.inventory else None

    # Filter products
    if args.product:
        products = [p for p in products if p["product_id"] == args.product]
        if not products:
            print(f"Error: product '{args.product}' not found", file=sys.stderr)
            sys.exit(1)

    # --- ABC Classification ---
    if orders:
        revenue_by_product = {}
        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)
        abc_lookup = {}
        for cls_name, cls_items in abc.items():
            for item in cls_items:
                abc_lookup[item["sku"]] = cls_name

        print(f"\nABC Classification ({len(items)} products)")
        print(f"  A: {len(abc['A'])} products ({sum(i['revenue'] for i in abc['A']):,.0f} revenue)")
        print(f"  B: {len(abc['B'])} products ({sum(i['revenue'] for i in abc['B']):,.0f} revenue)")
        print(f"  C: {len(abc['C'])} products ({sum(i['revenue'] for i in abc['C']):,.0f} revenue)")
    else:
        abc_lookup = {}

    # --- Latest inventory per product ---
    latest_stock = {}
    if inventory:
        for row in inventory:
            latest_stock[row["product_id"]] = int(row["closing_stock"])

    # --- Per-product optimization ---
    print(f"\n{'SKU':<16} {'ABC':>4} {'EOQ':>7} {'SS':>7} {'ROP':>7} {'Risk':>6} {'Stock':>7}")
    print("-" * 68)

    reports = []
    for p in products:
        pid = p["product_id"]
        daily_demand = float(p["base_daily_demand"])
        annual_demand = daily_demand * 365
        cost = float(p["cost"])
        lead_time = int(p["lead_time_days"])
        demand_std = daily_demand * 0.3  # rough estimate
        holding_cost = cost * 0.25
        ordering_cost = 50.0
        stock = latest_stock.get(pid, int(p["initial_stock"]))

        report = inv.full_report(
            sku=p["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=args.service_level,
        )
        report.abc_class = abc_lookup.get(pid, "—")
        reports.append(report)

        risk_str = f"{report.stockout_risk:.0%}" if report.stockout_risk is not None else "—"
        print(f"{report.sku:<16} {report.abc_class:>4} {report.eoq.order_quantity:>7.0f} "
              f"{report.safety_stock:>7.0f} {report.reorder_point:>7.0f} {risk_str:>6} {stock:>7}")

    # --- Alerts ---
    at_risk = [r for r in reports if r.stockout_risk and r.stockout_risk > 0.3]
    if at_risk:
        print(f"\n  ALERTS: {len(at_risk)} products at risk of stockout (>30%)")
        for r in sorted(at_risk, key=lambda r: r.stockout_risk, reverse=True)[:10]:
            print(f"    {r.sku}: {r.stockout_risk:.0%} risk — reorder at {r.reorder_point:.0f} units")

    # --- Newsvendor (if stockpyl available) ---
    if args.newsvendor and hasattr(inv, "newsvendor"):
        print(f"\nNewsvendor Analysis (single-period)")
        print(f"  {'SKU':<16} {'Optimal Q':>10} {'Crit Ratio':>12}")
        print("  " + "-" * 40)
        for p in products[:10]:
            daily_demand = float(p["base_daily_demand"])
            cost = float(p["cost"])
            price = float(p["price"])
            nv = inv.newsvendor(
                selling_price=price,
                cost=cost,
                demand_mean=daily_demand * 30,  # monthly
                demand_std=daily_demand * 0.3 * 30**0.5,
            )
            print(f"  {p['sku']:<16} {nv['optimal_quantity']:>10.0f} {nv['critical_ratio']:>12.3f}")


def add_forecast_parser(subparsers):
    """Add 'forecast' subcommand to the main CLI parser."""
    p = subparsers.add_parser("forecast", help="Demand forecasting on CSV data")
    p.add_argument("--data", required=True, help="Path to daily_demand.csv or similar time series CSV")
    p.add_argument("--product", default=None, help="Product ID to forecast")
    p.add_argument("--category", default=None, help="Forecast all products in category")
    p.add_argument("--top", type=int, default=None, help="Forecast top N products by volume")
    p.add_argument("--horizon", type=int, default=14, help="Days ahead to forecast (default: 14)")
    p.add_argument("--holdout", type=int, default=30, help="Days to hold out for accuracy eval (default: 30)")
    p.add_argument("--method", default=None,
                   help="Model: auto, moving_average, exponential_smoothing, croston, arima, ets, theta")
    p.add_argument("--backend", default=None, help="Backend: builtin or statsforecast")
    p.add_argument("--season-length", type=int, default=7, dest="season_length", help="Seasonal period (default: 7)")
    p.add_argument("--compare", action="store_true", help="Compare all available methods")
    p.add_argument("--quiet", action="store_true", help="Skip day-by-day detail output")
    p.add_argument("--output", default=None, help="Export results to JSON file")


def add_optimize_parser(subparsers):
    """Add 'optimize' subcommand to the main CLI parser."""
    p = subparsers.add_parser("optimize", help="Inventory optimization on CSV data")
    p.add_argument("--data", required=True, help="Path to products.csv")
    p.add_argument("--orders", default=None, help="Path to orders.csv (for ABC classification)")
    p.add_argument("--inventory", default=None, help="Path to inventory.csv (for stockout risk)")
    p.add_argument("--product", default=None, help="Single product ID to analyze")
    p.add_argument("--backend", default=None, help="Backend: builtin or stockpyl")
    p.add_argument("--service-level", type=float, default=0.95, dest="service_level",
                   help="Target service level for safety stock (default: 0.95)")
    p.add_argument("--newsvendor", action="store_true", help="Include newsvendor analysis (requires stockpyl)")


def add_recommend_parser(subparsers):
    """Add 'recommend' subcommand to the main CLI parser.

    TODO: Implement run_recommend once recommendation backends are ready.
    """
    p = subparsers.add_parser("recommend", help="Product recommendations from order history")
    p.add_argument("--orders", required=True, help="Path to orders.csv")
    p.add_argument("--products", default=None, help="Path to products.csv (for content features)")
    p.add_argument("--customer", default=None, help="Customer ID to get recommendations for")
    p.add_argument("--product", default=None, help="Product ID to find similar items for")
    p.add_argument("--similar", action="store_true", help="Find similar products (requires --product)")
    p.add_argument("--popular", action="store_true", help="Show most popular products")
    p.add_argument("--top", type=int, default=10, help="Number of recommendations (default: 10)")
    p.add_argument("--backend", default=None, help="Backend: implicit or lightfm")


def run_recommend(args):
    """Run product recommendations. TODO: implement once backends are ready."""
    print("Error: recommendations module is not yet implemented.", file=sys.stderr)
    print("  Planned backends: implicit (ALS/BPR), lightfm (hybrid)", file=sys.stderr)
    print("  Install with: uv pip install implicit lightfm", file=sys.stderr)
    sys.exit(1)


# ---------------------------------------------------------------------------
# Anomaly detection
# ---------------------------------------------------------------------------


def add_anomaly_parser(subparsers):
    """Add 'anomaly' subcommand to the main CLI parser."""
    p = subparsers.add_parser("anomaly", help="Detect demand anomalies (spikes/drops)")
    p.add_argument("--data", required=True, help="Path to daily_demand.csv")
    p.add_argument("--product", default=None, help="Product ID to analyze")
    p.add_argument("--method", default="zscore", choices=["zscore", "iqr"], help="Detection method (default: zscore)")
    p.add_argument("--window", type=int, default=30, help="Rolling window size in days (default: 30)")
    p.add_argument("--threshold", type=float, default=2.5, help="Z-score threshold (default: 2.5)")
    p.add_argument("--top", type=int, default=20, help="Max anomalies to display (default: 20)")


def run_anomaly(args):
    """Run anomaly detection on demand data."""
    from platoon.learning.providers import get_provider

    demand_rows = _load_csv(args.data)

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

    dates, series = _extract_series(demand_rows, product_id)

    ad = get_provider("anomaly")
    result = ad.detect(series, dates, method=args.method, window=args.window, threshold=args.threshold)

    print(f"\nAnomaly Detection: {product_id}")
    print(f"  Method: {result.method} | Window: {args.window} | Threshold: {args.threshold}")
    print(f"  Points: {result.total_points} | Anomalies: {result.anomaly_count} ({result.anomaly_rate:.1%})")
    print(f"  Mean: {result.series_mean:.1f} | Std: {result.series_std:.1f}")

    if result.anomalies:
        print(f"\n  {'Date':<12} {'Value':>8} {'Expected':>10} {'Z-score':>8} {'Dir':>6} {'Severity':>8}")
        print("  " + "-" * 56)
        for a in result.anomalies[:args.top]:
            print(f"  {a.date:<12} {a.value:>8.0f} {a.expected:>10.1f} {a.z_score:>8.2f} {a.direction:>6} {a.severity:>8}")
        if len(result.anomalies) > args.top:
            print(f"  ... ({len(result.anomalies) - args.top} more)")
    else:
        print("\n  No anomalies detected.")


# ---------------------------------------------------------------------------
# Basket analysis
# ---------------------------------------------------------------------------


def add_basket_parser(subparsers):
    """Add 'basket' subcommand to the main CLI parser."""
    p = subparsers.add_parser("basket", help="Basket analysis (frequently bought together)")
    p.add_argument("--orders", required=True, help="Path to orders.csv")
    p.add_argument("--min-support", type=float, default=0.01, dest="min_support", help="Min support (default: 0.01)")
    p.add_argument("--min-confidence", type=float, default=0.3, dest="min_confidence", help="Min confidence (default: 0.3)")
    p.add_argument("--top", type=int, default=20, help="Max rules to display (default: 20)")


def run_basket(args):
    """Run basket analysis on order data."""
    from platoon.learning.providers import get_provider

    rows = _load_csv(args.orders)

    # Group by order_id
    order_items: dict[str, set] = {}
    for r in rows:
        oid = r["order_id"]
        if oid not in order_items:
            order_items[oid] = set()
        order_items[oid].add(r["product_id"])

    transactions = [items for items in order_items.values() if len(items) >= 2]

    print(f"\nBasket Analysis")
    print(f"  Orders: {len(order_items)} | Multi-item: {len(transactions)}")

    ba = get_provider("basket")
    result = ba.analyze(transactions, min_support=args.min_support, min_confidence=args.min_confidence)

    print(f"  Rules found: {len(result.rules)}")

    if result.rules:
        print(f"\n  {'Antecedent':<14} {'→':>2} {'Consequent':<14} {'Support':>8} {'Conf':>6} {'Lift':>6}")
        print("  " + "-" * 56)
        for r in result.rules[:args.top]:
            print(f"  {r.antecedent:<14} → {r.consequent:<14} {r.support:>8.4f} {r.confidence:>6.2f} {r.lift:>6.2f}")
    else:
        print("\n  No rules found at current thresholds.")


# ---------------------------------------------------------------------------
# Cash flow
# ---------------------------------------------------------------------------


def add_cashflow_parser(subparsers):
    """Add 'cashflow' subcommand to the main CLI parser."""
    p = subparsers.add_parser("cashflow", help="Cash flow projection from products + demand")
    p.add_argument("--data", required=True, help="Path to products.csv")
    p.add_argument("--demand", required=True, help="Path to daily_demand.csv")
    p.add_argument("--inventory", default=None, help="Path to inventory.csv (enables reorder simulation)")
    p.add_argument("--horizon", type=int, default=30, help="Days to project (default: 30)")
    p.add_argument("--product", default=None, help="Single product ID")


def run_cashflow(args):
    """Run cash flow projection."""
    from platoon.mcp.controllers import cashflow as cashflow_ctrl

    result = cashflow_ctrl(
        args.data, args.demand,
        inventory_path=args.inventory,
        horizon=args.horizon,
        product_id=args.product,
    )

    if result["status"] != "ok":
        print(f"Error: {result['error']}", file=sys.stderr)
        sys.exit(1)

    s = result["summary"]
    print(f"\nCash Flow Projection ({result['horizon_days']} days)")
    print(f"  Revenue:       ${s['total_revenue']:>12,.2f}")
    print(f"  COGS:          ${s['total_cogs']:>12,.2f}")
    print(f"  Gross Profit:  ${s['total_gross_profit']:>12,.2f}")
    print(f"  Reorder Costs: ${s['total_reorder_costs']:>12,.2f}")
    print(f"  Net Cash:      ${s['net_cash']:>12,.2f}")
    print(f"  Avg Daily Rev: ${s['avg_daily_revenue']:>12,.2f}")

    if result["reorder_events"]:
        print(f"\n  Reorder Events: {len(result['reorder_events'])}")
        for e in result["reorder_events"][:10]:
            print(f"    Day {e['day']:>3}: {e['product_id']} — {e['quantity']} units (${e['cost']:,.2f})")


# ---------------------------------------------------------------------------
# Scheduling
# ---------------------------------------------------------------------------


def add_schedule_parser(subparsers):
    """Add 'schedule' subcommand to the main CLI parser."""
    p = subparsers.add_parser("schedule", help="Staff scheduling from demand + availability")
    p.add_argument("--demand", required=True, help="Path to daily_demand.csv")
    p.add_argument("--staff", required=True, help="Path to staff.csv")
    p.add_argument("--shift-hours", type=float, default=8, dest="shift_hours", help="Hours per shift (default: 8)")
    p.add_argument("--min-coverage", type=float, default=1.0, dest="min_coverage", help="Min coverage fraction (default: 1.0)")
    p.add_argument("--horizon", type=int, default=7, help="Days to schedule (default: 7)")
    p.add_argument("--product", default=None, help="Filter demand by product ID")


def run_schedule(args):
    """Run staff scheduling."""
    from platoon.mcp.controllers import schedule as schedule_ctrl

    result = schedule_ctrl(
        args.demand, args.staff,
        shift_hours=args.shift_hours,
        min_coverage=args.min_coverage,
        horizon_days=args.horizon,
        product_id=args.product,
    )

    if result["status"] != "ok":
        print(f"Error: {result['error']}", file=sys.stderr)
        sys.exit(1)

    print(f"\nStaff Schedule")
    print(f"  Total Cost:  ${result['total_cost']:>10,.2f}")
    print(f"  Total Hours: {result['total_hours']:>10.1f}")
    print(f"  Assignments: {result['assignment_count']}")

    if result["schedule_grid"]:
        print(f"\n  {'Day':<12} Staff")
        print("  " + "-" * 40)
        for day, staff in result["schedule_grid"].items():
            print(f"  {day:<12} {', '.join(staff)}")

    if result["coverage_gaps"]:
        print(f"\n  Coverage Gaps: {len(result['coverage_gaps'])}")
        for g in result["coverage_gaps"]:
            print(f"    {g['slot']}: need {g['needed']}h, have {g['assigned']}h (gap: {g['gap']}h)")
