"""
Dashboard domain: coffee analytics dashboard, settings, API keys.
"""
import asyncio
import hashlib
import secrets
from datetime import datetime
from pathlib import Path

from quart import (
    Blueprint,
    current_app,
    flash,
    g,
    redirect,
    render_template,
    request,
    url_for,
)

from .. import analytics
from ..auth.routes import login_required, update_user
from ..core import csrf_protect, execute, fetch_all, fetch_one, soft_delete

# Blueprint with its own template folder
bp = Blueprint(
    "dashboard",
    __name__,
    template_folder=str(Path(__file__).parent / "templates"),
    url_prefix="/dashboard",
)


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

async def get_user_stats(user_id: int) -> dict:
    """Get dashboard stats for user."""
    api_calls = await fetch_one(
        """
        SELECT COUNT(*) as count FROM api_requests
        WHERE user_id = ? AND created_at > date('now', '-30 days')
        """,
        (user_id,),
    )

    return {
        "api_calls": api_calls["count"] if api_calls else 0,
    }


# API Key queries
async def create_api_key(user_id: int, name: str, scopes: list[str] = None) -> tuple[str, int]:
    """Create API key. Returns (raw_key, key_id) - raw_key shown only once."""
    raw_key = f"sk_{secrets.token_urlsafe(32)}"
    key_hash = hashlib.sha256(raw_key.encode()).hexdigest()
    prefix = raw_key[:12]

    now = datetime.utcnow().isoformat()
    key_id = await execute(
        """
        INSERT INTO api_keys (user_id, name, key_hash, key_prefix, scopes, created_at)
        VALUES (?, ?, ?, ?, ?, ?)
        """,
        (user_id, name, key_hash, prefix, ",".join(scopes or ["read"]), now),
    )

    return raw_key, key_id


async def get_user_api_keys(user_id: int) -> list[dict]:
    """Get all API keys for user (without hashes)."""
    return await fetch_all(
        """
        SELECT id, name, key_prefix, scopes, created_at, last_used_at
        FROM api_keys
        WHERE user_id = ? AND deleted_at IS NULL
        ORDER BY created_at DESC
        """,
        (user_id,),
    )


async def delete_api_key(key_id: int, user_id: int) -> bool:
    """Delete API key (soft delete)."""
    result = await execute(
        """
        UPDATE api_keys
        SET deleted_at = ?
        WHERE id = ? AND user_id = ? AND deleted_at IS NULL
        """,
        (datetime.utcnow().isoformat(), key_id, user_id),
    )
    return result > 0


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

# Time range param → query parameter mapping
RANGE_MAP = {
    "3m":  {"days": 90,   "weeks": 13,  "months": 3,   "years": 3},
    "6m":  {"days": 180,  "weeks": 26,  "months": 6,   "years": 3},
    "1y":  {"days": 365,  "weeks": 52,  "months": 12,  "years": 5},
    "2y":  {"days": 730,  "weeks": 104, "months": 24,  "years": 5},
    "5y":  {"days": 1825, "weeks": 260, "months": 60,  "years": 5},
    "10y": {"days": 3650, "weeks": 520, "months": 120, "years": 10},
    "max": {"days": 3650, "weeks": 1040, "months": 360, "years": None},
}


def _safe(results: list, defaults: list):
    """Map asyncio.gather results, replacing exceptions with defaults and logging them."""
    out = []
    for i, (r, d) in enumerate(zip(results, defaults)):
        if isinstance(r, Exception):
            current_app.logger.warning("Analytics query %d failed: %s", i, r)
            out.append(d)
        else:
            out.append(r)
    return out


@bp.route("/")
@login_required
async def index():
    """Pulse — one-screen snapshot across all data domains."""
    user = g.user
    plan = (g.get("subscription") or {}).get("plan", "free")

    if analytics._db_path or analytics._conn is not None:
        results = await asyncio.gather(
            analytics.get_price_latest(analytics.COFFEE_TICKER),
            analytics.get_cot_positioning_latest(analytics.COFFEE_CFTC_CODE),
            analytics.get_ice_stocks_latest(),
            analytics.get_stock_to_use_trend(analytics.COFFEE_COMMODITY_CODE),
            analytics.get_price_time_series(analytics.COFFEE_TICKER, limit=90),
            analytics.get_ice_stocks_trend(days=90),
            analytics.get_cot_index_trend(analytics.COFFEE_CFTC_CODE, weeks=26),
            analytics.get_global_time_series(
                analytics.COFFEE_COMMODITY_CODE,
                ["production", "total_distribution"],
                start_year=(None if plan != "free" else None),
            ),
            analytics.get_weather_stress_latest(),
            analytics.get_weather_stress_trend(days=90),
            return_exceptions=True,
        )
        defaults = [None, None, None, [], [], [], [], [], None, []]
        (
            price_latest, cot_latest, ice_stocks_latest,
            stu_trend, price_series, ice_stocks_trend, cot_trend, time_series,
            weather_stress_latest, weather_stress_trend,
        ) = _safe(results, defaults)
    else:
        price_latest, cot_latest, ice_stocks_latest = None, None, None
        stu_trend, price_series, ice_stocks_trend, cot_trend, time_series = [], [], [], [], []
        weather_stress_latest, weather_stress_trend = None, []

    # Latest stock-to-use for the metric card
    stu_latest = stu_trend[-1] if stu_trend else None

    # Free plan: cap supply sparkline to last 5 years
    if plan == "free" and time_series:
        max_year = time_series[-1]["market_year"]
        time_series = [r for r in time_series if r["market_year"] >= max_year - 5]

    return await render_template(
        "pulse.html",
        user=user,
        plan=plan,
        price_latest=price_latest,
        cot_latest=cot_latest,
        ice_stocks_latest=ice_stocks_latest,
        stu_latest=stu_latest,
        price_series=price_series,
        ice_stocks_trend=ice_stocks_trend,
        cot_trend=cot_trend,
        time_series=time_series,
        weather_stress_latest=weather_stress_latest,
        weather_stress_trend=weather_stress_trend,
    )


@bp.route("/supply")
@login_required
async def supply():
    """Supply & demand deep-dive — USDA PSD fundamentals."""
    plan = (g.get("subscription") or {}).get("plan", "free")

    range_key = request.args.get("range", "5y")
    if range_key not in RANGE_MAP:
        range_key = "5y"
    # Free plan capped at 5y on supply page
    if plan == "free" and range_key in ("10y", "max"):
        range_key = "5y"

    metric = request.args.get("metric", "production")
    if metric not in analytics.ALLOWED_METRICS:
        metric = "production"

    rng = RANGE_MAP[range_key]
    start_year = None
    if rng["years"] is not None:
        import datetime
        current_year = datetime.date.today().year
        start_year = current_year - rng["years"]

    if analytics._db_path or analytics._conn is not None:
        results = await asyncio.gather(
            analytics.get_global_time_series(
                analytics.COFFEE_COMMODITY_CODE,
                ["production", "exports", "imports", "ending_stocks", "total_distribution"],
                start_year=start_year,
            ),
            analytics.get_stock_to_use_trend(analytics.COFFEE_COMMODITY_CODE),
            analytics.get_supply_demand_balance(analytics.COFFEE_COMMODITY_CODE),
            analytics.get_top_countries(analytics.COFFEE_COMMODITY_CODE, metric, limit=10),
            analytics.get_production_yoy_by_country(analytics.COFFEE_COMMODITY_CODE, limit=15),
            return_exceptions=True,
        )
        defaults = [[], [], [], [], []]
        time_series, stu_trend, balance, top_countries, yoy = _safe(results, defaults)

        # Apply year filter to stu/balance (get_global_time_series already filters)
        if start_year is not None:
            stu_trend = [r for r in stu_trend if r["market_year"] >= start_year]
            balance = [r for r in balance if r["market_year"] >= start_year]
    else:
        time_series, stu_trend, balance, top_countries, yoy = [], [], [], [], []

    ctx = dict(
        plan=plan,
        range_key=range_key,
        metric=metric,
        time_series=time_series,
        stu_trend=stu_trend,
        balance=balance,
        top_countries=top_countries,
        yoy=yoy,
    )

    if request.headers.get("HX-Request"):
        return await render_template("supply_canvas.html", **ctx)
    return await render_template("supply.html", user=g.user, **ctx)


@bp.route("/positioning")
@login_required
async def positioning():
    """Market positioning deep-dive — CFTC COT + KC=F price."""
    plan = (g.get("subscription") or {}).get("plan", "free")

    range_key = request.args.get("range", "1y")
    if range_key not in RANGE_MAP:
        range_key = "1y"

    cot_type = request.args.get("type", "fut")
    if cot_type not in ("fut", "combined"):
        cot_type = "fut"
    combined = cot_type == "combined"

    rng = RANGE_MAP[range_key]
    price_limit = rng["days"]
    cot_weeks = rng["weeks"]

    options_delta = None
    if analytics._db_path or analytics._conn is not None:
        gather_coros = [
            analytics.get_price_latest(analytics.COFFEE_TICKER),
            analytics.get_price_time_series(analytics.COFFEE_TICKER, limit=price_limit),
            analytics.get_cot_positioning_latest(analytics.COFFEE_CFTC_CODE, combined=combined),
            analytics.get_cot_index_trend(analytics.COFFEE_CFTC_CODE, weeks=cot_weeks, combined=combined),
        ]
        if combined:
            gather_coros.append(analytics.get_cot_options_delta(analytics.COFFEE_CFTC_CODE))

        results = await asyncio.gather(*gather_coros, return_exceptions=True)
        defaults = [None, [], None, [], None] if combined else [None, [], None, []]
        safe_results = _safe(results, defaults)

        price_latest, price_series, cot_latest, cot_trend = safe_results[:4]
        if combined:
            options_delta = safe_results[4]
    else:
        price_latest, price_series, cot_latest, cot_trend = None, [], None, []

    ctx = dict(
        plan=plan,
        range_key=range_key,
        cot_type=cot_type,
        price_latest=price_latest,
        price_series=price_series,
        cot_latest=cot_latest,
        cot_trend=cot_trend,
        options_delta=options_delta,
    )

    if request.headers.get("HX-Request"):
        return await render_template("positioning_canvas.html", **ctx)
    return await render_template("positioning.html", user=g.user, **ctx)


@bp.route("/warehouse")
@login_required
async def warehouse():
    """Warehouse deep-dive — ICE physical stocks."""
    plan = (g.get("subscription") or {}).get("plan", "free")

    range_key = request.args.get("range", "1y")
    view = request.args.get("view", "stocks")

    if range_key not in RANGE_MAP:
        range_key = "1y"
    if view not in ("stocks", "aging", "byport"):
        view = "stocks"

    rng = RANGE_MAP[range_key]

    stocks_latest = stocks_trend = aging_latest = byport_latest = byport_trend = None
    stocks_trend = aging_latest = byport_trend = []

    if analytics._db_path or analytics._conn is not None:
        if view == "stocks":
            results = await asyncio.gather(
                analytics.get_ice_stocks_latest(),
                analytics.get_ice_stocks_trend(days=rng["days"]),
                return_exceptions=True,
            )
            stocks_latest, stocks_trend = _safe(results, [None, []])
        elif view == "aging":
            aging_latest = await asyncio.gather(
                analytics.get_ice_aging_latest(),
                return_exceptions=True,
            )
            aging_latest = _safe(aging_latest, [[]])[0]
        elif view == "byport":
            results = await asyncio.gather(
                analytics.get_ice_stocks_by_port_latest(),
                analytics.get_ice_stocks_by_port_trend(months=rng["months"]),
                return_exceptions=True,
            )
            byport_latest, byport_trend = _safe(results, [None, []])

    ctx = dict(
        plan=plan,
        range_key=range_key,
        view=view,
        stocks_latest=stocks_latest,
        stocks_trend=stocks_trend,
        aging_latest=aging_latest,
        byport_latest=byport_latest,
        byport_trend=byport_trend,
    )

    if request.headers.get("HX-Request"):
        return await render_template("warehouse_canvas.html", **ctx)
    return await render_template("warehouse.html", user=g.user, **ctx)


@bp.route("/countries")
@login_required
async def countries():
    """Country comparison page."""
    user = g.user
    plan = (g.get("subscription") or {}).get("plan", "free")

    # Get available countries for coffee
    all_countries = await analytics.get_top_countries(analytics.COFFEE_COMMODITY_CODE, "production", limit=50)

    # Parse query params
    selected_codes = request.args.getlist("country")
    metric = request.args.get("metric", "production")

    comparison_data = []
    if selected_codes:
        selected_codes = selected_codes[:10]  # cap at 10
        comparison_data = await analytics.get_country_comparison(
            analytics.COFFEE_COMMODITY_CODE, selected_codes, metric
        )

    # HTMX partial: return just the canvas fragment (chips + chart)
    if request.headers.get("HX-Request"):
        return await render_template(
            "countries_canvas.html",
            all_countries=all_countries,
            selected_codes=selected_codes,
            metric=metric,
            comparison_data=comparison_data,
        )

    return await render_template(
        "countries.html",
        user=user,
        plan=plan,
        all_countries=all_countries,
        selected_codes=selected_codes,
        metric=metric,
        comparison_data=comparison_data,
    )


@bp.route("/weather")
@login_required
async def weather():
    """Weather & crop stress deep-dive — 12 coffee-growing origins."""
    plan = (g.get("subscription") or {}).get("plan", "free")

    range_key = request.args.get("range", "1y")
    if range_key not in RANGE_MAP:
        range_key = "1y"
    location_id = request.args.get("location", "").strip()

    rng = RANGE_MAP[range_key]
    days = rng["days"]

    if analytics._db_path or analytics._conn is not None:
        if location_id:
            results = await asyncio.gather(
                analytics.get_weather_stress_latest(),
                analytics.get_weather_locations(),
                analytics.get_weather_stress_trend(days=days),
                analytics.get_weather_active_alerts(),
                analytics.get_weather_location_series(location_id, days=days),
                return_exceptions=True,
            )
            defaults = [None, [], [], [], []]
            stress_latest, locations, stress_trend, active_alerts, location_series = _safe(
                results, defaults
            )
        else:
            results = await asyncio.gather(
                analytics.get_weather_stress_latest(),
                analytics.get_weather_locations(),
                analytics.get_weather_stress_trend(days=days),
                analytics.get_weather_active_alerts(),
                return_exceptions=True,
            )
            defaults = [None, [], [], []]
            stress_latest, locations, stress_trend, active_alerts = _safe(results, defaults)
            location_series = []
    else:
        stress_latest = None
        locations = stress_trend = active_alerts = location_series = []

    # Resolve the selected location's metadata for the detail header
    location_detail = None
    if location_id and locations:
        matches = [loc for loc in locations if loc["location_id"] == location_id]
        location_detail = matches[0] if matches else None

    ctx = dict(
        plan=plan,
        range_key=range_key,
        location_id=location_id,
        location_detail=location_detail,
        stress_latest=stress_latest,
        locations=locations,
        stress_trend=stress_trend,
        active_alerts=active_alerts,
        location_series=location_series,
    )

    if request.headers.get("HX-Request"):
        return await render_template("weather_canvas.html", **ctx)
    return await render_template("weather.html", user=g.user, **ctx)


@bp.route("/settings", methods=["GET", "POST"])
@login_required
@csrf_protect
async def settings():
    """User settings page."""
    if request.method == "POST":
        form = await request.form

        # Update user settings
        await update_user(
            g.user["id"],
            name=form.get("name", "").strip() or None,
            updated_at=datetime.utcnow().isoformat(),
        )

        await flash("Settings saved!", "success")
        return redirect(url_for("dashboard.settings"))

    user = g.user
    subscription = g.get("subscription")
    api_keys = await get_user_api_keys(g.user["id"])

    return await render_template(
        "settings.html",
        user=user,
        subscription=subscription,
        api_keys=api_keys,
    )


@bp.route("/api-keys", methods=["POST"])
@login_required
@csrf_protect
async def create_key():
    """Create new API key."""
    form = await request.form
    name = form.get("name", "").strip() or "Untitled Key"
    scopes = form.getlist("scopes") or ["read"]

    raw_key, key_id = await create_api_key(g.user["id"], name, scopes)

    await flash(f"API key created! Copy it now, it won't be shown again: {raw_key}", "success")
    return redirect(url_for("dashboard.settings") + "#api-keys")


@bp.route("/api-keys/<int:key_id>/delete", methods=["POST"])
@login_required
@csrf_protect
async def delete_key(key_id: int):
    """Delete API key."""
    success = await delete_api_key(key_id, g.user["id"])

    if success:
        await flash("API key deleted.", "success")
    else:
        await flash("Could not delete API key.", "error")

    return redirect(url_for("dashboard.settings") + "#api-keys")


@bp.route("/delete-account", methods=["POST"])
@login_required
@csrf_protect
async def delete_account():
    """Delete user account (soft delete)."""
    from quart import session

    await soft_delete("users", g.user["id"])
    session.clear()

    await flash("Your account has been deleted.", "info")
    return redirect(url_for("public.landing"))
