"""
Merlya Commands - System handlers.

Implements /scan, /health, and /log commands.
"""

from __future__ import annotations

import asyncio
import json
from typing import TYPE_CHECKING, Any

from merlya.commands.handlers.scan_format import (
    ScanOptions,
    ScanResult,
    format_scan_output,
    parse_scan_args,
    scan_to_dict,
)
from merlya.commands.registry import CommandResult, command
from merlya.ssh.pool import SSHConnectionOptions

if TYPE_CHECKING:
    from merlya.core.context import SharedContext

# Limit concurrent SSH channels to avoid MaxSessions limit (default 10 in OpenSSH)
MAX_CONCURRENT_SSH_CHANNELS = 6


@command("scan", "Scan a host for system info and security", "/scan <host> [options]")
async def cmd_scan(ctx: SharedContext, args: list[str]) -> CommandResult:
    """
    Scan a host for system information and security issues.

    Options:
      --full       Complete scan (default)
      --quick      Fast check: CPU, memory, disk, ports only
      --security   Security checks only
      --system     System checks only
      --json       Output as JSON
      --all-disks  Check all mounted filesystems
      --show-all   Show all ports and users (no truncation)
      --no-docker  Skip Docker checks
      --no-updates Skip pending updates check
    """
    if not args:
        return CommandResult(
            success=False,
            message="Usage: `/scan <host> [<host2> ...] [--full|--quick|--security|--system] [--json]`\n"
            "Example: `/scan @myserver` or `/scan @myserver @myserver2 --quick --json`",
            show_help=True,
        )

    host_names, opts = parse_scan_args(args)
    if not host_names:
        return CommandResult(
            success=False,
            message="No host specified. Usage: `/scan <host> [<host2> ...] [--full|--quick|--security|--system] [--json]`",
            show_help=True,
        )

    outputs: list[str] = []
    results: list[ScanResult] = []
    successes = 0

    for host_name in host_names:
        host = await ctx.hosts.get_by_name(host_name)
        if not host:
            outputs.append(
                f"❌ Host '{host_name}' not found. Use `/hosts add {host_name}` to add it."
            )
            continue

        ctx.ui.info(f"Scanning {host.name} ({host.hostname})...")

        # Establish connection once
        try:
            with ctx.ui.spinner(f"Connecting to {host.hostname}..."):
                ssh_pool = await ctx.get_ssh_pool()
                connect_timeout = min(ctx.config.ssh.connect_timeout, 15)
                options = SSHConnectionOptions(
                    port=host.port,
                    jump_host=host.jump_host,
                    connect_timeout=connect_timeout,
                )
                await ssh_pool.get_connection(
                    host=host.hostname,
                    username=host.username,
                    private_key=host.private_key,
                    options=options,
                    host_name=host.name,  # Pass inventory name for credential lookup
                )
        except Exception as e:
            outputs.append(f"❌ Unable to connect to `{host.name}` ({host.hostname}): {e}")
            continue

        # Run scan based on type
        scan_result = ScanResult()

        # Shared semaphore to limit total concurrent SSH channels
        ssh_semaphore = asyncio.Semaphore(MAX_CONCURRENT_SSH_CHANNELS)

        with ctx.ui.spinner(f"Scanning {host.name}..."):
            if opts.scan_type == "quick":
                await _scan_quick(ctx, host, scan_result)
            elif opts.scan_type == "system":
                await _scan_system_parallel(ctx, host, scan_result, opts, ssh_semaphore)
            elif opts.scan_type == "security":
                await _scan_security_parallel(ctx, host, scan_result, opts, ssh_semaphore)
            else:  # full
                await asyncio.gather(
                    _scan_system_parallel(ctx, host, scan_result, opts, ssh_semaphore),
                    _scan_security_parallel(ctx, host, scan_result, opts, ssh_semaphore),
                )

        # Calculate severity score using embeddings if available
        await _calculate_severity_score(ctx, scan_result)
        results.append(scan_result)
        successes += 1

        # Format output
        if opts.output_json:
            outputs.append(f"```json\n{json.dumps(scan_to_dict(scan_result, host), indent=2)}\n```")
        else:
            outputs.append(format_scan_output(scan_result, host, opts))

    return CommandResult(
        success=successes > 0,
        message="\n\n".join(outputs),
        data=results if len(results) > 1 else (results[0] if results else None),
    )


async def _scan_quick(ctx: SharedContext, host: Any, result: ScanResult) -> None:
    """Quick scan: CPU, memory, disk, ports only (parallel)."""
    from merlya.tools.security import check_open_ports
    from merlya.tools.system import check_cpu, check_disk_usage, check_memory

    # Run all checks in parallel
    mem_task = check_memory(ctx, host.name)
    cpu_task = check_cpu(ctx, host.name)
    disk_task = check_disk_usage(ctx, host.name, "/")
    ports_task = check_open_ports(ctx, host.name)

    mem_result, cpu_result, disk_result, ports_result = await asyncio.gather(
        mem_task, cpu_task, disk_task, ports_task
    )

    # Process results
    if mem_result.success and mem_result.data:
        result.sections["memory"] = mem_result.data
        if mem_result.data.get("warning"):
            result.warning_count += 1
            result.issues.append(
                {
                    "type": "memory",
                    "severity": "warning",
                    "message": f"Memory usage high: {mem_result.data.get('use_percent')}%",
                }
            )

    if cpu_result.success and cpu_result.data:
        result.sections["cpu"] = cpu_result.data
        if cpu_result.data.get("warning"):
            result.warning_count += 1
            result.issues.append(
                {
                    "type": "cpu",
                    "severity": "warning",
                    "message": f"CPU load high: {cpu_result.data.get('use_percent')}%",
                }
            )

    if disk_result.success and disk_result.data:
        result.sections["disk"] = disk_result.data
        if disk_result.data.get("warning"):
            result.warning_count += 1
            result.issues.append(
                {
                    "type": "disk",
                    "severity": "warning",
                    "message": f"Disk usage high: {disk_result.data.get('use_percent')}%",
                }
            )

    if ports_result.success and ports_result.data:
        result.sections["ports"] = ports_result.data


async def _scan_system_parallel(
    ctx: SharedContext,
    host: Any,
    result: ScanResult,
    opts: ScanOptions,
    semaphore: asyncio.Semaphore | None = None,
) -> None:
    """System scan with parallel execution."""
    from merlya.tools.system import (
        check_all_disks,
        check_cpu,
        check_docker,
        check_memory,
        get_system_info,
    )

    sem = semaphore or asyncio.Semaphore(MAX_CONCURRENT_SSH_CHANNELS)

    async def run_with_sem(coro: Any) -> Any:
        async with sem:
            return await coro

    # Build task list based on options
    tasks = {
        "system_info": run_with_sem(get_system_info(ctx, host.name)),
        "memory": run_with_sem(check_memory(ctx, host.name)),
        "cpu": run_with_sem(check_cpu(ctx, host.name)),
    }

    if opts.all_disks:
        tasks["disks"] = run_with_sem(check_all_disks(ctx, host.name))
    else:
        from merlya.tools.system import check_disk_usage

        tasks["disk"] = run_with_sem(check_disk_usage(ctx, host.name, "/"))

    if opts.include_docker:
        tasks["docker"] = run_with_sem(check_docker(ctx, host.name))

    # Execute all tasks in parallel (semaphore limits concurrency)
    results_dict = {}
    task_list = list(tasks.items())
    task_results = await asyncio.gather(*[t[1] for t in task_list], return_exceptions=True)

    for (name, _), res in zip(task_list, task_results, strict=False):
        if isinstance(res, BaseException):
            continue
        if hasattr(res, "success") and res.success and hasattr(res, "data") and res.data:
            results_dict[name] = res.data
            # Check for warnings
            if isinstance(res.data, dict) and res.data.get("warning"):
                result.warning_count += 1
                result.issues.append(
                    {
                        "type": name,
                        "severity": "warning",
                        "message": f"{name.title()} threshold exceeded",
                    }
                )

    result.sections["system"] = results_dict


async def _scan_security_parallel(
    ctx: SharedContext,
    host: Any,
    result: ScanResult,
    opts: ScanOptions,
    semaphore: asyncio.Semaphore | None = None,
) -> None:
    """Security scan with parallel execution."""
    from merlya.tools.security import (
        audit_ssh_keys,
        check_critical_services,
        check_failed_logins,
        check_open_ports,
        check_pending_updates,
        check_security_config,
        check_sudo_config,
        check_users,
    )

    sem = semaphore or asyncio.Semaphore(MAX_CONCURRENT_SSH_CHANNELS)

    async def run_with_sem(coro: Any) -> Any:
        async with sem:
            return await coro

    # Build task list
    tasks = {
        "ports": run_with_sem(check_open_ports(ctx, host.name)),
        "ssh_config": run_with_sem(check_security_config(ctx, host.name)),
        "users": run_with_sem(check_users(ctx, host.name)),
        "ssh_keys": run_with_sem(audit_ssh_keys(ctx, host.name)),
        "sudo": run_with_sem(check_sudo_config(ctx, host.name)),
        "services": run_with_sem(check_critical_services(ctx, host.name)),
    }

    if opts.include_logins:
        tasks["failed_logins"] = run_with_sem(check_failed_logins(ctx, host.name))

    if opts.include_updates:
        tasks["updates"] = run_with_sem(check_pending_updates(ctx, host.name))

    # Execute all in parallel (semaphore limits concurrency)
    results_dict: dict[str, Any] = {}
    task_list = list(tasks.items())
    task_results = await asyncio.gather(*[t[1] for t in task_list], return_exceptions=True)

    for (name, _), res in zip(task_list, task_results, strict=False):
        if isinstance(res, BaseException):
            continue
        if hasattr(res, "success") and res.success:
            results_dict[name] = getattr(res, "data", None)
            # Count severity
            severity = getattr(res, "severity", "info")
            if severity == "critical":
                result.critical_count += 1
                result.issues.append({"type": name, "severity": "critical", "data": res.data})
            elif severity == "warning":
                result.warning_count += 1
                result.issues.append({"type": name, "severity": "warning", "data": res.data})

    result.sections["security"] = results_dict


async def _calculate_severity_score(ctx: SharedContext, result: ScanResult) -> None:
    """Calculate severity score, optionally using embeddings for intelligent analysis."""
    # Base scoring
    base_score = result.critical_count * 25 + result.warning_count * 10
    result.severity_score = min(100, base_score)

    # Try embedding-based severity analysis
    try:
        if hasattr(ctx, "_router") and ctx._router and ctx._router.model_loaded:
            # Use embeddings to analyze issue severity
            issue_texts = [
                issue.get("message", str(issue.get("data", "")))[:200] for issue in result.issues
            ]
            if issue_texts:
                # Get embeddings for issues and compare to critical patterns
                classifier = ctx._router.classifier
                critical_patterns = [
                    "critical security vulnerability",
                    "root access compromised",
                    "unauthorized access detected",
                    "system completely down",
                ]
                # This is a simplified version - could be extended
                for issue_text in issue_texts:
                    issue_emb = await classifier._get_embedding(issue_text)
                    if issue_emb is not None:
                        for pattern in critical_patterns:
                            pattern_emb = await classifier._get_embedding(pattern)
                            if pattern_emb is not None:
                                sim = classifier._cosine_similarity(issue_emb, pattern_emb)
                                if sim > 0.7:
                                    result.severity_score = min(100, result.severity_score + 15)
                                    break
    except Exception:
        pass  # Fallback to base scoring if embeddings fail


@command("health", "Show system health status", "/health")
async def cmd_health(_ctx: SharedContext, _args: list[str]) -> CommandResult:
    """Show system health status."""
    from merlya.health import run_startup_checks

    health = await run_startup_checks()

    lines = ["**Health Status**\n"]
    for check in health.checks:
        icon = "✓" if check.status.value == "ok" else "✗"
        lines.append(f"  {icon} {check.message}")

    if health.capabilities:
        lines.append("\n**Capabilities:**")
        for cap, enabled in health.capabilities.items():
            status = "enabled" if enabled else "disabled"
            lines.append(f"  {cap}: `{status}`")

    return CommandResult(success=True, message="\n".join(lines), data=health)


@command("log", "Configure logging", "/log <subcommand>")
async def cmd_log(ctx: SharedContext, args: list[str]) -> CommandResult:
    """Configure logging settings."""
    if not args:
        return _show_log_config(ctx)

    action = args[0].lower()

    if action == "level" and len(args) > 1:
        return _set_log_level(ctx, args[1])
    elif action == "show":
        return _show_recent_logs(ctx)

    return CommandResult(success=False, message="Unknown log command. Use `/log` for help.")


def _show_log_config(ctx: SharedContext) -> CommandResult:
    """Show logging configuration."""
    config = ctx.config.logging
    return CommandResult(
        success=True,
        message=f"**Logging Configuration**\n\n"
        f"  - Level: `{config.file_level}`\n"
        f"  - Max size: `{config.max_size_mb}MB`\n"
        f"  - Retention: `{config.retention_days} days`\n"
        f"  - Max files: `{config.max_files}`\n\n"
        "Use `/log level <debug|info|warning|error>` to change.",
    )


def _set_log_level(ctx: SharedContext, level_str: str) -> CommandResult:
    """Set logging level."""
    level = level_str.lower()
    if level not in ("debug", "info", "warning", "error"):
        return CommandResult(
            success=False,
            message="Valid levels: `debug`, `info`, `warning`, `error`",
        )

    ctx.config.logging.file_level = level  # type: ignore[assignment]
    ctx.config.save()

    from merlya.core.logging import configure_logging

    configure_logging(console_level=level, file_level=level, force=True)

    return CommandResult(success=True, message=f"✅ Log level set to `{level}`")


def _show_recent_logs(ctx: SharedContext) -> CommandResult:
    """Show recent log entries."""
    log_path = ctx.config.general.data_dir / "logs" / "merlya.log"
    if log_path.exists():
        lines = log_path.read_text().split("\n")[-20:]
        return CommandResult(
            success=True,
            message=f"**Recent logs** ({log_path})\n\n```\n" + "\n".join(lines) + "\n```",
        )
    return CommandResult(success=False, message="No log file found.")
