"""Preflight checks — validate all prerequisites before starting V3Q.AI.

Run automatically on startup. Catches missing dependencies, bad config,
unreachable services, and missing database schemas early.
"""

from __future__ import annotations

import asyncio
import os
import sys
from pathlib import Path


class PreflightError:
    """A single preflight check failure."""

    def __init__(self, component: str, message: str, fix: str, critical: bool = True) -> None:
        self.component = component
        self.message = message
        self.fix = fix
        self.critical = critical

    def __str__(self) -> str:
        severity = "CRITICAL" if self.critical else "WARNING"
        return f"[{severity}] {self.component}: {self.message}\n  Fix: {self.fix}"


class PreflightResult:
    """Collection of preflight check results."""

    def __init__(self) -> None:
        self.errors: list[PreflightError] = []
        self.passed: list[str] = []

    def add_error(self, component: str, message: str, fix: str, critical: bool = True) -> None:
        self.errors.append(PreflightError(component, message, fix, critical))

    def add_pass(self, component: str) -> None:
        self.passed.append(component)

    @property
    def has_critical_errors(self) -> bool:
        return any(e.critical for e in self.errors)

    def print_report(self) -> None:
        """Print a human-readable preflight report."""
        print("\n" + "=" * 60)
        print("  V3Q.AI — Preflight Check Report")
        print("=" * 60)

        if self.passed:
            print(f"\n  Passed ({len(self.passed)}):")
            for p in self.passed:
                print(f"    [OK] {p}")

        if self.errors:
            warnings = [e for e in self.errors if not e.critical]
            criticals = [e for e in self.errors if e.critical]

            if warnings:
                print(f"\n  Warnings ({len(warnings)}):")
                for e in warnings:
                    print(f"    [WARN] {e.component}: {e.message}")
                    print(f"           Fix: {e.fix}")

            if criticals:
                print(f"\n  Critical Errors ({len(criticals)}):")
                for e in criticals:
                    print(f"    [FAIL] {e.component}: {e.message}")
                    print(f"           Fix: {e.fix}")

        print()
        if self.has_critical_errors:
            print("  STATUS: FAILED — Cannot start V3Q.AI")
            print("  Fix the critical errors above and try again.")
        else:
            warnings_count = len([e for e in self.errors if not e.critical])
            if warnings_count:
                print(f"  STATUS: OK (with {warnings_count} warnings)")
            else:
                print("  STATUS: ALL CHECKS PASSED")
        print("=" * 60 + "\n")


async def run_preflight() -> PreflightResult:
    """Run all preflight checks and return results."""
    result = PreflightResult()

    # 1. Check Python version
    _check_python_version(result)

    # 2. Check config files exist
    _check_config_files(result)

    # 3. Check required Python packages
    _check_packages(result)

    # 4. Load config and check values
    config = _check_config_values(result)

    # 5. Check PostgreSQL connection
    if config:
        await _check_postgresql(result, config)

    # 6. Check database schemas
    if config:
        await _check_schemas(result, config)

    # 7. Check Ollama availability
    await _check_ollama(result, config)

    # 8. Check Anthropic API key
    _check_api_key(result, config)

    # 9. Check results directory
    _check_results_dir(result, config)

    # 10. Check SMTP configuration (optional)
    _check_smtp(result, config)

    return result


def _check_python_version(result: PreflightResult) -> None:
    """Verify Python 3.12+."""
    v = sys.version_info
    if v.major < 3 or (v.major == 3 and v.minor < 12):
        result.add_error(
            "Python",
            f"Python 3.12+ required, found {v.major}.{v.minor}.{v.micro}",
            "Install Python 3.12 or newer from python.org",
        )
    else:
        result.add_pass(f"Python {v.major}.{v.minor}.{v.micro}")


def _check_config_files(result: PreflightResult) -> None:
    """Verify config files exist."""
    config_dir = Path(__file__).resolve().parent.parent / "config"

    if not (config_dir / "app.yaml").exists():
        result.add_error(
            "Config",
            "config/app.yaml not found",
            "Copy config/app.yaml.example to config/app.yaml and edit settings",
        )
    else:
        result.add_pass("config/app.yaml exists")

    env_file = config_dir / ".env"
    if not env_file.exists():
        result.add_error(
            "Config",
            "config/.env not found",
            "Create config/.env with ANTHROPIC_API_KEY=your-key-here",
        )
    else:
        result.add_pass("config/.env exists")


def _check_packages(result: PreflightResult) -> None:
    """Check critical Python packages are installed."""
    packages = {
        "fastapi": "pip install fastapi",
        "uvicorn": "pip install uvicorn[standard]",
        "sqlalchemy": "pip install sqlalchemy[asyncio]",
        "asyncpg": "pip install asyncpg",
        "anthropic": "pip install anthropic",
        "httpx": "pip install httpx",
        "pandas": "pip install pandas",
        "sqlparse": "pip install sqlparse",
        "yaml": "pip install pyyaml",
        "dotenv": "pip install python-dotenv",
    }

    missing = []
    for pkg, install_cmd in packages.items():
        try:
            __import__(pkg)
        except ImportError:
            missing.append((pkg, install_cmd))

    if missing:
        names = ", ".join(p[0] for p in missing)
        result.add_error(
            "Packages",
            f"Missing Python packages: {names}",
            "pip install -r requirements.txt",
        )
    else:
        result.add_pass("All required Python packages installed")


def _check_config_values(result: PreflightResult) -> object | None:
    """Load and validate configuration."""
    try:
        from config import get_settings
        settings = get_settings()
        result.add_pass("Configuration loaded successfully")
        return settings
    except Exception as e:
        result.add_error(
            "Config",
            f"Failed to load configuration: {e}",
            "Check config/app.yaml for syntax errors",
        )
        return None


async def _check_postgresql(result: PreflightResult, config) -> None:
    """Test PostgreSQL connection."""
    try:
        import asyncpg
        conn = await asyncio.wait_for(
            asyncpg.connect(
                host=config.db.host,
                port=config.db.port,
                database=config.db.database,
                user=config.db.user,
                password=config.db.password or None,
            ),
            timeout=5,
        )
        version = await conn.fetchval("SELECT version()")
        await conn.close()
        # Extract short version
        short_ver = version.split(",")[0] if version else "unknown"
        result.add_pass(f"PostgreSQL connected ({short_ver})")
    except asyncio.TimeoutError:
        result.add_error(
            "PostgreSQL",
            f"Connection timed out to {config.db.host}:{config.db.port}",
            "Ensure PostgreSQL is running: brew services start postgresql (macOS) or net start postgresql (Windows)",
        )
    except Exception as e:
        error_msg = str(e)
        if "does not exist" in error_msg:
            result.add_error(
                "PostgreSQL",
                f"Database '{config.db.database}' does not exist",
                f"Create it: createdb {config.db.database}",
            )
        elif "password authentication failed" in error_msg:
            result.add_error(
                "PostgreSQL",
                "Password authentication failed",
                "Check POSTGRES_PASSWORD in config/.env or pg_hba.conf authentication settings",
            )
        elif "Connection refused" in error_msg:
            result.add_error(
                "PostgreSQL",
                f"Cannot connect to PostgreSQL at {config.db.host}:{config.db.port}",
                "Start PostgreSQL: brew services start postgresql (macOS) or net start postgresql-x64-17 (Windows)",
            )
        else:
            result.add_error("PostgreSQL", f"Connection failed: {error_msg}", "Check database configuration in config/app.yaml")


async def _check_schemas(result: PreflightResult, config) -> None:
    """Verify V3Q schemas exist in PostgreSQL."""
    try:
        import asyncpg
        conn = await asyncpg.connect(
            host=config.db.host,
            port=config.db.port,
            database=config.db.database,
            user=config.db.user,
            password=config.db.password or None,
        )

        schemas = await conn.fetch(
            "SELECT nspname FROM pg_catalog.pg_namespace WHERE nspname LIKE 'v3q_%'"
        )
        schema_names = {row["nspname"] for row in schemas}
        await conn.close()

        required = {"v3q_config", "v3q_data", "v3q_results"}
        missing = required - schema_names

        if missing:
            result.add_error(
                "Database Schemas",
                f"Missing schemas: {', '.join(sorted(missing))}",
                "Run: psql -U <user> -d v3q -f scripts/setup_postgresql.sql",
            )
        else:
            result.add_pass("All V3Q schemas exist (v3q_config, v3q_data, v3q_results)")

            # Check critical tables
            conn = await asyncpg.connect(
                host=config.db.host,
                port=config.db.port,
                database=config.db.database,
                user=config.db.user,
                password=config.db.password or None,
            )
            tables = await conn.fetch("""
                SELECT table_schema || '.' || table_name as full_name
                FROM information_schema.tables
                WHERE table_schema LIKE 'v3q_%'
            """)
            table_names = {row["full_name"] for row in tables}
            await conn.close()

            required_tables = {
                "v3q_config.database_connections",
                "v3q_config.table_registry",
                "v3q_config.column_registry",
                "v3q_config.audit_log",
                "v3q_config.app_settings",
                "v3q_results.conversations",
                "v3q_results.messages",
            }
            missing_tables = required_tables - table_names
            if missing_tables:
                result.add_error(
                    "Database Tables",
                    f"Missing tables: {', '.join(sorted(missing_tables))}",
                    "Run: psql -U <user> -d v3q -f scripts/setup_postgresql.sql",
                )
            else:
                result.add_pass(f"All required tables exist ({len(table_names)} tables)")

    except Exception as e:
        # Don't add another error if PG connection already failed
        if not any(err.component == "PostgreSQL" for err in result.errors):
            result.add_error("Database Schemas", f"Schema check failed: {e}", "Ensure PostgreSQL is running")


async def _check_ollama(result: PreflightResult, config) -> None:
    """Check if Ollama is running and has the required model."""
    try:
        import httpx
        base_url = config.ollama.base_url if config else "http://localhost:11434"
        model = config.ollama.model if config else "llama3.1:8b"

        async with httpx.AsyncClient(timeout=5) as client:
            resp = await client.get(f"{base_url}/api/tags")
            if resp.status_code != 200:
                result.add_error(
                    "Ollama",
                    f"Ollama API returned status {resp.status_code}",
                    "Start Ollama: ollama serve",
                    critical=False,
                )
                return

            data = resp.json()
            model_names = [m["name"] for m in data.get("models", [])]

            if model in model_names:
                result.add_pass(f"Ollama running with model '{model}'")
            else:
                # Check if model is available with a different tag
                base_model = model.split(":")[0]
                available = [m for m in model_names if m.startswith(base_model)]
                if available:
                    result.add_error(
                        "Ollama",
                        f"Model '{model}' not found. Available: {', '.join(available)}",
                        f"Update ollama.model in config/app.yaml to one of: {', '.join(available)}",
                        critical=False,
                    )
                else:
                    result.add_error(
                        "Ollama",
                        f"Model '{model}' not installed",
                        f"Install it: ollama pull {model}",
                        critical=False,
                    )
    except Exception:
        result.add_error(
            "Ollama",
            "Cannot connect to Ollama at localhost:11434",
            "Start Ollama: ollama serve (or install from ollama.com)",
            critical=False,
        )


def _check_api_key(result: PreflightResult, config) -> None:
    """Check Anthropic API key is configured."""
    if not config:
        return

    key = config.claude.api_key
    if not key:
        result.add_error(
            "Anthropic API Key",
            "No API key configured",
            "Set ANTHROPIC_API_KEY in config/.env",
        )
    elif not key.startswith("sk-ant-"):
        result.add_error(
            "Anthropic API Key",
            "API key format looks invalid (should start with 'sk-ant-')",
            "Check your API key in config/.env",
            critical=False,
        )
    else:
        result.add_pass(f"Anthropic API key configured ({key[:12]}...)")


def _check_results_dir(result: PreflightResult, config) -> None:
    """Check/create results directory."""
    if not config:
        return

    results_path = Path(config.results_csv_path)
    try:
        results_path.mkdir(parents=True, exist_ok=True)
        result.add_pass(f"Results directory: {results_path}")
    except Exception as e:
        result.add_error(
            "Results Directory",
            f"Cannot create results directory: {e}",
            f"Manually create: mkdir -p {results_path}",
        )


def _check_smtp(result: PreflightResult, config) -> None:
    """Check if SMTP is configured for notifications (non-critical)."""
    if not config:
        return

    try:
        notif = config.notifications
        if notif.enabled:
            smtp = notif.smtp
            if not smtp.host:
                result.add_error(
                    "SMTP",
                    "Notifications enabled but SMTP host not configured",
                    "Set notifications.smtp.host in config/app.yaml or notification_settings table",
                    critical=False,
                )
            elif not smtp.from_email:
                result.add_error(
                    "SMTP",
                    "SMTP from_email not configured",
                    "Set notifications.smtp.from_email in config/app.yaml",
                    critical=False,
                )
            else:
                result.add_pass(f"SMTP configured ({smtp.host}:{smtp.port})")
        else:
            result.add_pass("Notifications disabled (can be enabled in settings)")
    except AttributeError:
        # notifications config section doesn't exist yet
        result.add_pass("Notifications not configured (optional)")


async def run_and_report() -> bool:
    """Run preflight checks, print report, return True if OK to proceed."""
    result = await run_preflight()
    result.print_report()
    return not result.has_critical_errors
