"""MixLift MCP Server — Marketing Mix Modeling for AI assistants.

Provides a single tool `mixlift_analyze` that runs full Bayesian MMM analysis
on marketing CSV data and returns ROAS, budget optimization, and saturation curves.
"""

from __future__ import annotations

import json
import logging
import os
from pathlib import Path

import pandas as pd
from mcp.server.fastmcp import FastMCP

from mixlift_mcp.license import LicenseStatus, free_tier, validate_license

logger = logging.getLogger(__name__)

DEMO_CSV_PATH = Path(__file__).parent / "data" / "demo.csv"

mcp = FastMCP("mixlift")

# ---------------------------------------------------------------------------
# Path validation guardrails
# ---------------------------------------------------------------------------

#: File extensions that are allowed as CSV inputs.
_ALLOWED_EXTENSIONS = {".csv", ".tsv", ".txt"}

#: Path components that indicate sensitive locations. Checked against every
#: part of the resolved path (case-insensitive) so that tricks like
#: ``../../.ssh/id_rsa`` are caught after resolution.
_BLOCKED_PATH_FRAGMENTS = {
    ".ssh",
    ".env",
    ".git",
    "credentials",
    "secrets",
    ".aws",
    ".config",
    ".gnupg",
    ".gpg",
    "keychain",
    "id_rsa",
    "id_ed25519",
    "id_ecdsa",
    "id_dsa",
    "authorized_keys",
    "known_hosts",
}


def _validate_csv_path(csv_path: str) -> tuple[Path, str | None]:
    """Validate and resolve a user-supplied CSV path.

    Returns ``(resolved_path, None)`` on success, or ``(Path(), error_message)``
    on failure.

    Guardrails applied (in order):
    1. Extension must be in ``_ALLOWED_EXTENSIONS``.
    2. Resolved absolute path must not contain any ``_BLOCKED_PATH_FRAGMENTS``.
    3. If the environment variable ``MIXLIFT_ALLOWED_DIRS`` is set (colon-separated
       list of directories), the resolved path must fall within one of them.
    """
    resolved = Path(csv_path).expanduser().resolve()

    # 1. Extension check
    if resolved.suffix.lower() not in _ALLOWED_EXTENSIONS:
        return Path(), (
            f"Invalid file extension '{resolved.suffix}'. "
            f"Only {', '.join(sorted(_ALLOWED_EXTENSIONS))} files are allowed."
        )

    # 2. Sensitive-path check — inspect every part of the resolved path
    path_str_lower = str(resolved).lower()
    for fragment in _BLOCKED_PATH_FRAGMENTS:
        if fragment in path_str_lower:
            logger.warning("Blocked sensitive path access attempt: %s", resolved)
            return Path(), (
                f"Access denied: the path contains a restricted component ('{fragment}'). "
                "Please use a file outside sensitive system directories."
            )

    # 3. Allowed-directories check (opt-in via environment variable)
    allowed_dirs_env = os.environ.get("MIXLIFT_ALLOWED_DIRS", "").strip()
    if allowed_dirs_env:
        allowed_dirs = [
            Path(d).expanduser().resolve()
            for d in allowed_dirs_env.split(":")
            if d.strip()
        ]
        if not any(
            resolved == allowed or allowed in resolved.parents
            for allowed in allowed_dirs
        ):
            allowed_display = ", ".join(str(d) for d in allowed_dirs)
            logger.warning(
                "Blocked path outside allowed directories: %s (allowed: %s)",
                resolved,
                allowed_display,
            )
            return Path(), (
                f"Access denied: '{resolved}' is not inside an allowed directory. "
                f"Allowed directories: {allowed_display}"
            )

    logger.info("Resolved CSV path: %s", resolved)
    return resolved, None


def _detect_csv_format(df: pd.DataFrame) -> str:
    """Detect whether CSV is wide-format (demo-style) or platform export.

    Returns: 'wide', 'meta', 'google', 'tiktok', or 'generic'
    """
    cols_lower = {c.lower() for c in df.columns}

    # Wide format: has columns ending in _spend and a revenue/target column
    spend_cols = [c for c in df.columns if c.lower().endswith("_spend")]
    if len(spend_cols) >= 2 and any(
        c in cols_lower for c in ("revenue", "target", "sales")
    ):
        return "wide"

    # Delegate to the existing ingest service for platform detection
    from mixlift.ingest.service import detect_platform

    return detect_platform(df)


def _load_wide_format(df: pd.DataFrame) -> tuple[pd.DataFrame, pd.Series, list[str]]:
    """Load wide-format CSV (like the bundled demo data) directly into model inputs.

    Wide format has: date, <channel>_spend, ..., revenue
    """
    from mixlift.modeling.preprocessing import add_control_columns

    df = df.copy()
    df["date"] = pd.to_datetime(df["date"])

    channel_columns = [c for c in df.columns if c.endswith("_spend")]

    # Target column
    target_col = None
    for candidate in ("revenue", "target", "sales"):
        if candidate in df.columns:
            target_col = candidate
            break

    if target_col is None:
        raise ValueError(
            "Wide-format CSV must have a 'revenue', 'target', or 'sales' column."
        )

    y = df[target_col].astype(float)

    # Keep only date + spend columns, add controls
    X = df[["date"] + channel_columns].copy()
    X = add_control_columns(X)

    return X, y, channel_columns


def _load_platform_csv(
    df: pd.DataFrame, platform: str
) -> tuple[pd.DataFrame, pd.Series, list[str]]:
    """Load a platform-specific or generic CSV via the ingest pipeline."""
    from mixlift.ingest.service import parse_csv, validate_data
    from mixlift.modeling.preprocessing import prepare_model_data

    canonical, detected = parse_csv(df, platform=platform)

    errors = validate_data(canonical)
    if errors:
        raise ValueError(
            "Data validation failed:\n" + "\n".join(f"  - {e}" for e in errors)
        )

    X, y, channel_columns = prepare_model_data(canonical)
    return X, y, channel_columns


def _enforce_tier_limits(
    license_status: LicenseStatus,
    n_channels: int,
    n_rows: int,
) -> None:
    """Raise ValueError if data exceeds tier limits."""
    if n_channels > license_status.max_channels:
        raise ValueError(
            f"Free tier allows up to {license_status.max_channels} channels, "
            f"but your data has {n_channels}. "
            f"Upgrade to Pro ($199/mo) at https://mixlift.io/pricing"
        )
    if n_rows > license_status.max_rows:
        raise ValueError(
            f"Free tier allows up to {license_status.max_rows} rows, "
            f"but your data has {n_rows}. "
            f"Upgrade to Pro ($199/mo) at https://mixlift.io/pricing"
        )


def _build_result(
    model_results,
    optimization: dict,
    saturation_curves: dict,
    channel_columns: list[str],
    license_status: LicenseStatus,
    csv_format: str,
) -> dict:
    """Build the final result dictionary."""
    from mixlift.modeling.recommendations import (
        generate_recommendations,
        format_all_recommendations,
    )

    # Generate recommendations
    recommendations, summary = generate_recommendations(
        current_allocation=optimization["current_allocation"],
        optimal_allocation=optimization["optimal_allocation"],
        roas_estimates=model_results.roas_estimates,
        expected_lift_pct=optimization["expected_lift_pct"],
    )

    return {
        "status": "success",
        "license": {
            "tier": "pro" if license_status.is_pro else "free",
            "message": license_status.message,
        },
        "data": {
            "format_detected": csv_format,
            "channels": list(model_results.roas_estimates.keys()),
            "n_channels": len(channel_columns),
        },
        "roas": {
            "estimates": model_results.roas_estimates,
            "confidence_intervals": {
                k: {"low": v[0], "high": v[1]}
                for k, v in model_results.roas_ci.items()
            },
        },
        "channel_contributions": {
            "estimates": model_results.channel_contributions,
            "confidence_intervals": {
                k: {"low": v[0], "high": v[1]}
                for k, v in model_results.channel_contributions_ci.items()
            },
        },
        "budget_optimization": {
            "current_allocation": optimization["current_allocation"],
            "optimal_allocation": optimization["optimal_allocation"],
            "total_budget": optimization["total_budget"],
            "expected_lift_pct": optimization["expected_lift_pct"],
        },
        "recommendations": {
            "summary": summary,
            "actions": [
                {
                    "rank": r.rank,
                    "action": r.action,
                    "channel": r.channel,
                    "change_amount": r.change_amount,
                    "current_spend": r.current_spend,
                    "optimal_spend": r.optimal_spend,
                    "roas": r.roas,
                    "reason": r.reason,
                }
                for r in recommendations
            ],
        },
        "saturation_curves": saturation_curves,
        "marginal_roas": {
            "estimates": model_results.marginal_roas_estimates,
            "confidence_intervals": {
                k: {"low": v[0], "high": v[1]}
                for k, v in model_results.marginal_roas_ci.items()
            },
        },
        "diagnostics": {
            "convergence_warnings": model_results.convergence_warnings,
        },
        "model_info": {
            "duration_secs": round(model_results.duration_secs, 1),
            "total_spend": model_results.total_spend,
        },
    }


def _run_pipeline(X, y, channel_columns):
    """Wrapper for run_full_pipeline (enables easy mocking in tests)."""
    from mixlift.modeling.engine import run_full_pipeline

    return run_full_pipeline(X, y, channel_columns)


def _run_optimizer(mmm, X, channel_columns):
    """Wrapper for optimize_budget (enables easy mocking in tests)."""
    from mixlift.modeling.optimizer import optimize_budget

    return optimize_budget(mmm, X, channel_columns)


def _run_saturation(mmm, X, channel_columns):
    """Wrapper for extract_saturation_curves (enables easy mocking in tests)."""
    from mixlift.modeling.engine import extract_saturation_curves

    return extract_saturation_curves(mmm, X, channel_columns)


@mcp.tool()
async def mixlift_analyze(
    csv_path: str = "",
    license_key: str = "",
) -> str:
    """Run full Bayesian Marketing Mix Model analysis on marketing spend data.

    Accepts a CSV file path with marketing data (Meta Ads, Google Ads, TikTok Ads,
    or generic format). Auto-detects the CSV format. If no path is provided, uses
    a bundled demo dataset.

    Returns channel ROAS, budget optimization recommendations, and saturation
    curve data as JSON.

    Args:
        csv_path: Path to a CSV file with marketing data. Leave empty to use demo data.
        license_key: Optional MixLift Pro license key. Free tier: 3 channels, 1000 rows.
    """
    # Validate license
    license_status = await validate_license(license_key if license_key else None)

    # Load data
    use_demo = not csv_path
    if use_demo:
        if not DEMO_CSV_PATH.exists():
            return json.dumps(
                {"status": "error", "message": "Demo data not found. Please provide a csv_path."}
            )
        csv_file = DEMO_CSV_PATH
    else:
        csv_file, validation_error = _validate_csv_path(csv_path)
        if validation_error:
            return json.dumps({"status": "error", "message": validation_error})
        if not csv_file.exists():
            return json.dumps(
                {"status": "error", "message": f"File not found: {csv_file}"}
            )

    try:
        df = pd.read_csv(csv_file)
    except Exception as e:
        return json.dumps(
            {"status": "error", "message": f"Failed to read CSV: {e}"}
        )

    # Detect format and load
    csv_format = _detect_csv_format(df)

    try:
        if csv_format == "wide":
            X, y, channel_columns = _load_wide_format(df)
        else:
            X, y, channel_columns = _load_platform_csv(df, csv_format)
    except ValueError as e:
        return json.dumps({"status": "error", "message": str(e)})

    # Enforce tier limits
    n_channels = len(channel_columns)
    n_rows = len(X)
    try:
        _enforce_tier_limits(license_status, n_channels, n_rows)
    except ValueError as e:
        return json.dumps({"status": "error", "message": str(e)})

    # Run the modeling pipeline
    try:
        logger.info(
            f"Running MMM: {n_channels} channels, {n_rows} rows, "
            f"format={csv_format}, tier={'pro' if license_status.is_pro else 'free'}"
        )

        model_results = _run_pipeline(X, y, channel_columns)

        optimization = _run_optimizer(model_results.mmm, X, channel_columns)

        saturation_curves = _run_saturation(model_results.mmm, X, channel_columns)

        result = _build_result(
            model_results=model_results,
            optimization=optimization,
            saturation_curves=saturation_curves,
            channel_columns=channel_columns,
            license_status=license_status,
            csv_format=csv_format,
        )

        return json.dumps(result, indent=2, default=str)

    except Exception as e:
        logger.exception("MMM analysis failed")
        return json.dumps(
            {"status": "error", "message": f"Analysis failed: {e}"}
        )


def main():
    """Entry point for the MixLift MCP server."""
    mcp.run(transport="stdio")


if __name__ == "__main__":
    main()
