"""
API domain: REST API for commodity analytics with key authentication and rate limiting.
"""
import csv
import hashlib
import io
from datetime import datetime
from functools import wraps

from quart import Blueprint, Response, g, jsonify, request

from .. import analytics
from ..core import check_rate_limit, execute, fetch_one

bp = Blueprint("api", __name__)


# =============================================================================
# SQL Queries
# =============================================================================

async def verify_api_key(raw_key: str) -> dict | None:
    """Verify API key and return key data with user info."""
    key_hash = hashlib.sha256(raw_key.encode()).hexdigest()

    result = await fetch_one(
        """
        SELECT ak.*, u.email, u.id as user_id,
               COALESCE(s.plan, 'free') as plan
        FROM api_keys ak
        JOIN users u ON u.id = ak.user_id
        LEFT JOIN subscriptions s ON s.user_id = u.id AND s.status = 'active'
        WHERE ak.key_hash = ? AND ak.deleted_at IS NULL AND u.deleted_at IS NULL
        """,
        (key_hash,),
    )

    if result:
        await execute(
            "UPDATE api_keys SET last_used_at = ? WHERE id = ?",
            (datetime.utcnow().isoformat(), result["id"]),
        )

    return result


async def log_api_request(user_id: int, endpoint: str, method: str) -> None:
    """Log API request for analytics and rate limiting."""
    await execute(
        """
        INSERT INTO api_requests (user_id, endpoint, method, created_at)
        VALUES (?, ?, ?, ?)
        """,
        (user_id, endpoint, method, datetime.utcnow().isoformat()),
    )


# =============================================================================
# Decorators
# =============================================================================

def api_key_required(scopes: list[str] = None):
    """Require valid API key with optional scope check."""
    def decorator(f):
        @wraps(f)
        async def decorated(*args, **kwargs):
            auth = request.headers.get("Authorization", "")
            if not auth.startswith("Bearer "):
                return jsonify({"error": "Missing API key"}), 401

            raw_key = auth[7:]
            key_data = await verify_api_key(raw_key)

            if not key_data:
                return jsonify({"error": "Invalid API key"}), 401

            # Check scopes
            if scopes:
                key_scopes = (key_data.get("scopes") or "").split(",")
                if not any(s in key_scopes for s in scopes):
                    return jsonify({"error": "Insufficient permissions"}), 403

            # Check plan allows API access
            plan = key_data.get("plan", "free")
            if plan == "free":
                return jsonify({"error": "API access requires a Starter or Pro plan"}), 403

            # Rate limiting
            rate_key = f"api:{key_data['id']}"
            allowed, info = await check_rate_limit(rate_key, limit=1000, window=3600)

            if not allowed:
                response = jsonify({"error": "Rate limit exceeded", **info})
                response.headers["X-RateLimit-Limit"] = str(info["limit"])
                response.headers["X-RateLimit-Remaining"] = str(info["remaining"])
                response.headers["X-RateLimit-Reset"] = str(info["reset"])
                return response, 429

            await log_api_request(key_data["user_id"], request.path, request.method)

            g.api_key = key_data
            g.user_id = key_data["user_id"]
            g.plan = plan

            return await f(*args, **kwargs)
        return decorated
    return decorator


# =============================================================================
# Routes
# =============================================================================

@bp.route("/me")
@api_key_required()
async def me():
    """Get current user info."""
    return jsonify({
        "user_id": g.user_id,
        "email": g.api_key["email"],
        "plan": g.plan,
        "key_name": g.api_key["name"],
        "scopes": g.api_key["scopes"].split(","),
    })


@bp.route("/commodities")
@api_key_required(scopes=["read"])
async def list_commodities():
    """List available commodities."""
    commodities = await analytics.get_available_commodities()
    return jsonify({"commodities": commodities})


@bp.route("/commodities/<int:code>/metrics")
@api_key_required(scopes=["read"])
async def commodity_metrics(code: int):
    """Time series metrics for a commodity. Query params: metrics, start_year, end_year."""
    raw_metrics = request.args.getlist("metrics") or ["production", "exports", "imports", "ending_stocks"]
    metrics = [m for m in raw_metrics if m in analytics.ALLOWED_METRICS]
    if not metrics:
        return jsonify({"error": f"No valid metrics. Allowed: {sorted(analytics.ALLOWED_METRICS)}"}), 400

    start_year = request.args.get("start_year", type=int)
    end_year = request.args.get("end_year", type=int)

    data = await analytics.get_global_time_series(code, metrics, start_year, end_year)
    return jsonify({"commodity_code": code, "metrics": metrics, "data": data})


@bp.route("/commodities/<int:code>/countries")
@api_key_required(scopes=["read"])
async def commodity_countries(code: int):
    """Country ranking for a commodity. Query params: metric, limit."""
    metric = request.args.get("metric", "production")
    if metric not in analytics.ALLOWED_METRICS:
        return jsonify({"error": f"Invalid metric. Allowed: {sorted(analytics.ALLOWED_METRICS)}"}), 400

    limit = min(int(request.args.get("limit", 20)), 100)

    data = await analytics.get_top_countries(code, metric, limit)
    return jsonify({"commodity_code": code, "metric": metric, "data": data})


@bp.route("/commodities/<code>/positioning")
@api_key_required(scopes=["read"])
async def commodity_positioning(code: str):
    """COT trader positioning time series for a commodity.

    Query params:
      metrics    — repeated param, e.g. ?metrics=managed_money_net&metrics=cot_index_26w
      start_date — ISO date filter (YYYY-MM-DD)
      end_date   — ISO date filter (YYYY-MM-DD)
      limit      — max rows returned (default 260, max 2000)
      type       — report variant: "fut" (futures-only, default) or "combined" (futures+options)
    """
    raw_metrics = request.args.getlist("metrics") or [
        "managed_money_net", "prod_merc_net", "open_interest", "cot_index_26w"
    ]
    metrics = [m for m in raw_metrics if m in analytics.ALLOWED_COT_METRICS]
    if not metrics:
        return jsonify({"error": f"No valid metrics. Allowed: {sorted(analytics.ALLOWED_COT_METRICS)}"}), 400

    start_date = request.args.get("start_date")
    end_date = request.args.get("end_date")
    limit = min(int(request.args.get("limit", 260)), 2000)
    cot_type = request.args.get("type", "fut")
    combined = cot_type == "combined"

    data = await analytics.get_cot_positioning_time_series(code, metrics, start_date, end_date, limit, combined=combined)
    return jsonify({"cftc_commodity_code": code, "type": cot_type, "metrics": metrics, "data": data})


@bp.route("/commodities/<code>/positioning/latest")
@api_key_required(scopes=["read"])
async def commodity_positioning_latest(code: str):
    """Latest week's full COT positioning snapshot for a commodity.

    Query params:
      type — report variant: "fut" (futures-only, default) or "combined" (futures+options)
    """
    cot_type = request.args.get("type", "fut")
    combined = cot_type == "combined"
    data = await analytics.get_cot_positioning_latest(code, combined=combined)
    if not data:
        return jsonify({"error": "No positioning data found for this commodity"}), 404
    return jsonify({"cftc_commodity_code": code, "type": cot_type, "data": data})


@bp.route("/commodities/<code>/prices")
@api_key_required(scopes=["read"])
async def commodity_prices(code: str):
    """Daily OHLCV price time series for a commodity ticker (e.g. KC=F).

    Query params:
      start_date — ISO date filter (YYYY-MM-DD)
      end_date   — ISO date filter (YYYY-MM-DD)
      limit      — max rows returned (default 504 ≈ 2 years, max 5000)
    """
    limit = min(int(request.args.get("limit", 504)), 5000)
    start_date = request.args.get("start_date")
    end_date = request.args.get("end_date")

    data = await analytics.get_price_time_series(code, limit=limit)

    # Apply date filters in Python — simpler than adding optional params to the query
    if start_date:
        data = [r for r in data if str(r["trade_date"]) >= start_date]
    if end_date:
        data = [r for r in data if str(r["trade_date"]) <= end_date]

    return jsonify({"ticker": code, "data": data})


@bp.route("/commodities/<code>/stocks")
@api_key_required(scopes=["read"])
async def commodity_ice_stocks(code: str):
    """ICE certified warehouse stock time series.

    Query params:
      start_date — ISO date filter (YYYY-MM-DD)
      end_date   — ISO date filter (YYYY-MM-DD)
      days       — trailing days (default 365, max 3650)
    """
    days = min(int(request.args.get("days", 365)), 3650)
    start_date = request.args.get("start_date")
    end_date = request.args.get("end_date")

    data = await analytics.get_ice_stocks_trend(days=days)

    if start_date:
        data = [r for r in data if str(r["report_date"]) >= start_date]
    if end_date:
        data = [r for r in data if str(r["report_date"]) <= end_date]

    return jsonify({"commodity": code, "data": data})


@bp.route("/commodities/<code>/stocks/aging")
@api_key_required(scopes=["read"])
async def commodity_ice_aging(code: str):
    """ICE certified stock aging report — age distribution across delivery ports.

    Query params:
      reports    — number of monthly snapshots to return (default 1, max 12)
      start_date — ISO date filter (YYYY-MM-DD)
      end_date   — ISO date filter (YYYY-MM-DD)
    """
    reports = min(int(request.args.get("reports", 1)), 12)
    start_date = request.args.get("start_date")
    end_date = request.args.get("end_date")

    if reports == 1:
        data = await analytics.get_ice_aging_latest()
    else:
        data = await analytics.get_ice_aging_trend(reports=reports)

    if start_date:
        data = [r for r in data if str(r["report_date"]) >= start_date]
    if end_date:
        data = [r for r in data if str(r["report_date"]) <= end_date]

    return jsonify({"commodity": code, "data": data})


@bp.route("/commodities/<code>/stocks/by-port")
@api_key_required(scopes=["read"])
async def commodity_ice_stocks_by_port(code: str):
    """ICE certified warehouse stocks by delivery port — monthly, back to 1996.

    Query params:
      months     — trailing months (default 120, max 360)
      start_date — ISO date filter (YYYY-MM-DD)
      end_date   — ISO date filter (YYYY-MM-DD)
    """
    months = min(int(request.args.get("months", 120)), 360)
    start_date = request.args.get("start_date")
    end_date = request.args.get("end_date")

    data = await analytics.get_ice_stocks_by_port_trend(months=months)

    if start_date:
        data = [r for r in data if str(r["report_date"]) >= start_date]
    if end_date:
        data = [r for r in data if str(r["report_date"]) <= end_date]

    return jsonify({"commodity": code, "data": data})


@bp.route("/weather/locations")
@api_key_required(scopes=["read"])
async def weather_locations():
    """12 coffee-growing locations with latest-day stress index, sorted by severity."""
    data = await analytics.get_weather_locations()
    return jsonify({"data": data})


@bp.route("/weather/locations/<location_id>")
@api_key_required(scopes=["read"])
async def weather_location_series(location_id: str):
    """Daily weather time series for one location.

    Query params:
      metrics — repeated param, e.g. ?metrics=crop_stress_index&metrics=precipitation_mm
      days    — trailing days (default 365, max 3650)
    """
    raw_metrics = request.args.getlist("metrics") or None
    if raw_metrics:
        metrics = [m for m in raw_metrics if m in analytics.ALLOWED_WEATHER_METRICS]
        if not metrics:
            return jsonify({"error": f"No valid metrics. Allowed: {sorted(analytics.ALLOWED_WEATHER_METRICS)}"}), 400
    else:
        metrics = None

    days = min(int(request.args.get("days", 365)), 3650)
    data = await analytics.get_weather_location_series(location_id, metrics=metrics, days=days)
    if not data:
        return jsonify({"error": f"No data found for location_id {location_id!r}"}), 404
    return jsonify({"location_id": location_id, "data": data})


@bp.route("/weather/stress")
@api_key_required(scopes=["read"])
async def weather_stress():
    """Daily global crop stress trend (avg + max across 12 origins).

    Query params:
      days — trailing days (default 365, max 3650)
    """
    days = min(int(request.args.get("days", 365)), 3650)
    data = await analytics.get_weather_stress_trend(days=days)
    return jsonify({"data": data})


@bp.route("/weather/alerts")
@api_key_required(scopes=["read"])
async def weather_alerts():
    """Locations with active crop stress flags on the latest observation date."""
    data = await analytics.get_weather_active_alerts()
    return jsonify({"data": data})


@bp.route("/commodities/<int:code>/metrics.csv")
@api_key_required(scopes=["read"])
async def commodity_metrics_csv(code: int):
    """CSV export of time series metrics."""
    if g.plan == "free":
        return jsonify({"error": "CSV export requires a Starter or Pro plan"}), 403

    raw_metrics = request.args.getlist("metrics") or [
        "production", "exports", "imports", "ending_stocks", "total_distribution",
    ]
    metrics = [m for m in raw_metrics if m in analytics.ALLOWED_METRICS]
    if not metrics:
        return jsonify({"error": "No valid metrics"}), 400

    data = await analytics.get_global_time_series(code, metrics)

    output = io.StringIO()
    if data:
        writer = csv.DictWriter(output, fieldnames=data[0].keys())
        writer.writeheader()
        writer.writerows(data)

    return Response(
        output.getvalue(),
        mimetype="text/csv",
        headers={"Content-Disposition": f"attachment; filename=commodity_{code}_metrics.csv"},
    )
