"""Core harvesting logic: SSH, SCP, version detection, filename building."""

from __future__ import annotations

import os
import re
import sqlite3
import subprocess
from datetime import UTC, datetime
from pathlib import Path

from loguru import logger

from harvest_session_dbs.core.tools import get_tool_info


def sanitize_hostname(host: str) -> str:
    """Replace dots and dashes in hostname with underscores for filename safety.

    Args:
        host: Raw hostname (e.g. "srv-fedora.fritz.box").

    Returns:
        Sanitized hostname (e.g. "srv_fedora_fritz_box").
    """
    return host.replace(".", "_").replace("-", "_")


def extract_version(output: str) -> str:
    """Extract a version string from command output.

    Handles formats like:
        devin 3000.2.17 (2c489dfc)      -> 3000.2.17
        1.18.3                            -> 1.18.3
        GitHub Copilot CLI 1.0.46.        -> 1.0.46

    Args:
        output: Raw command output string.

    Returns:
        Extracted version string, or "unknown" if no version found.
    """
    match = re.search(r"(\d+\.\d+\.\d+(?:\.\d+)*)", output)
    if match:
        return match.group(1)
    return "unknown"


def is_local_host(host: str) -> bool:
    """Check if a host refers to the local machine.

    Matches against:
    - ``localhost`` / ``127.0.0.1``
    - The full hostname (e.g. ``srv-fedora.fritz.box``)
    - The short hostname (e.g. ``srv-fedora``)

    This ensures that SSH config aliases like ``srv-fedora`` (which may resolve
    to a different network interface) are still detected as local, avoiding
    unnecessary SSH/SCP roundtrips for a 2GB file that's already on this disk.

    Args:
        host: Hostname string.

    Returns:
        True if the host refers to the local machine.
    """
    full_name = os.uname().nodename
    short_name = full_name.split(".")[0] if "." in full_name else full_name
    local_names = {"localhost", "127.0.0.1", full_name, short_name}
    return host in local_names


def get_remote_version(host: str, tool: str, version_cmd: list[str]) -> str:
    """Get the tool version from a host via SSH (or locally).

    Args:
        host: Target hostname.
        tool: Tool name (for logging).
        version_cmd: Command to run to get the version.

    Returns:
        Extracted version string, or "unknown" if detection fails.
    """
    if is_local_host(host):
        try:
            result = subprocess.run(version_cmd, capture_output=True, text=True, timeout=30)
            if result.returncode == 0:
                return extract_version(result.stdout)
        except (subprocess.TimeoutExpired, FileNotFoundError):
            pass
        return "unknown"

    # Run the version command through the user's login + interactive shell
    # so that .zshrc/.bashrc, mise shims, ~/.local/bin, etc. are all loaded.
    # A plain "ssh host cmd" or "ssh -t host cmd" does NOT source .zshrc
    # because zsh only loads .zshrc for interactive shells, and the command
    # passed to ssh runs as a non-interactive script.
    # Using $SHELL -l -i -c ensures both .zprofile/.zshrc (or .bash_profile/
    # .bashrc) are sourced, matching what the user sees in a real terminal.
    remote_cmd = " ".join(version_cmd)
    wrapped = f"exec $SHELL -l -i -c {remote_cmd!r}"
    ssh_cmd = [
        "ssh",
        "-o",
        "BatchMode=yes",
        "-o",
        "ConnectTimeout=10",
        host,
        wrapped,
    ]
    try:
        result = subprocess.run(ssh_cmd, capture_output=True, text=True, timeout=30)
        if result.returncode == 0:
            return extract_version(result.stdout)
    except (subprocess.TimeoutExpired, FileNotFoundError):
        pass
    return "unknown"


def scp_from_host(host: str, remote_path: str, dest: Path) -> bool:
    """Copy a file from a remote host via scp (or local copy).

    For remote hosts, the remote_path is first resolved through the user's
    login + interactive shell (so ``~``, ``$XDG_DATA_HOME``, mise paths, etc.
    are expanded correctly), then the absolute path is used for SCP.

    Args:
        host: Target hostname.
        remote_path: Path on the remote host (with ~ or env var expansion).
        dest: Local destination path.

    Returns:
        True if the copy succeeded, False otherwise.
    """
    dest.parent.mkdir(parents=True, exist_ok=True)

    if is_local_host(host):
        expanded = os.path.expanduser(remote_path)
        src = Path(expanded)
        if not src.exists():
            logger.warning(f"Local file not found: {src}")
            return False
        subprocess.run(["cp", str(src), str(dest)], check=True)
        logger.info(f"Copied local {src} -> {dest} ({dest.stat().st_size} bytes)")
        return True

    # Resolve the remote path through the user's login + interactive shell
    # so that ~, $XDG_DATA_HOME, and other env vars are expanded correctly.
    # A plain "ssh host 'echo ~/path'" does NOT source .zshrc/.bashrc.
    wrapped = f"exec $SHELL -l -i -c {f'echo {remote_path}'!r}"
    resolve_cmd = [
        "ssh",
        "-o",
        "BatchMode=yes",
        "-o",
        "ConnectTimeout=10",
        host,
        wrapped,
    ]
    try:
        result = subprocess.run(resolve_cmd, capture_output=True, text=True, timeout=30)
        if result.returncode == 0:
            resolved_path = result.stdout.strip().splitlines()[-1].strip()
            if resolved_path and not resolved_path.startswith("exec"):
                remote_path = resolved_path
    except subprocess.TimeoutExpired:
        pass  # Fall back to the original path (tilde expansion still works)

    cmd = [
        "scp",
        "-o",
        "BatchMode=yes",
        "-o",
        "ConnectTimeout=10",
        f"{host}:{remote_path}",
        str(dest),
    ]
    logger.debug(f"Running: {' '.join(cmd)}")
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
    except subprocess.TimeoutExpired:
        logger.warning(f"Timeout copying from {host}")
        return False
    if result.returncode != 0:
        logger.warning(f"Failed to copy from {host}: {result.stderr.strip()}")
        return False
    if not dest.exists() or dest.stat().st_size == 0:
        logger.warning(f"File from {host} is empty or missing")
        return False
    logger.info(f"Collected {host} -> {dest} ({dest.stat().st_size} bytes)")
    return True


def build_filename(tool: str, host: str, version: str, date_str: str) -> str:
    """Build the fixture filename: <tool>.<host>.<version>.<date>.db

    Args:
        tool: Tool name (e.g. "devin").
        host: Hostname (will be sanitized).
        version: Tool version string.
        date_str: Date in YYYY-MM-DD format.

    Returns:
        Filename string.
    """
    safe_host = sanitize_hostname(host)
    return f"{tool}.{safe_host}.{version}.{date_str}.db"


def harvest_from_host(
    host: str,
    tool: str,
    output_dir: Path,
    date_str: str,
    dry_run: bool = False,
    dump_schema: bool = False,
) -> dict[str, str]:
    """Harvest a single tool's DB from a single host.

    Args:
        host: Target hostname.
        tool: Tool name.
        output_dir: Directory to store the fixture file.
        date_str: Date string for the filename.
        dry_run: If True, don't actually copy files.
        dump_schema: If True, also dump the DB schema to a .sql file alongside.

    Returns:
        Dict with keys: tool, host, version, filename, status.
    """
    info = get_tool_info(tool)
    if info is None:
        logger.warning(f"Unknown tool: {tool}")
        return {"tool": tool, "host": host, "version": "", "filename": "", "status": "skipped"}

    remote_path, version_cmd = info
    version = get_remote_version(host, tool, version_cmd)
    filename = build_filename(tool, host, version, date_str)
    dest = output_dir / filename

    result = {
        "tool": tool,
        "host": host,
        "version": version,
        "filename": filename,
        "status": "",
    }

    if dry_run:
        logger.info(f"  [DRY] {tool} @ {host} v{version} -> {filename}")
        result["status"] = "dry-run"
        return result

    if scp_from_host(host, remote_path, dest):
        result["status"] = "ok"
        if dump_schema:
            dump_schema_file(dest)
    else:
        result["status"] = "failed"

    return result


def dump_schema_file(db_path: Path) -> bool:
    """Dump the schema of a SQLite DB to a .sql file alongside it.

    Uses Python's built-in ``sqlite3`` module to extract the schema via
    ``SELECT sql FROM sqlite_master WHERE type IN ('table','index','view','trigger')``.
    The output file is named ``<db_stem>.schema.sql``
    (e.g. ``devin.srv_fedora.3000.2.17.2026-07-23.schema.sql``).

    Args:
        db_path: Path to the SQLite database file.

    Returns:
        True if the schema was dumped successfully, False otherwise.
    """
    schema_path = db_path.with_suffix(".schema.sql")
    if not db_path.exists():
        logger.warning(f"Database file not found: {db_path}")
        return False
    try:
        conn = sqlite3.connect(str(db_path))
        rows = conn.execute(
            "SELECT sql FROM sqlite_master "
            "WHERE type IN ('table','index','view','trigger') "
            "AND sql IS NOT NULL ORDER BY type, name"
        ).fetchall()
        conn.close()
        schema = "\n".join(row[0] + ";\n" for row in rows)
        schema_path.write_text(schema)
        logger.info(f"Schema dumped: {schema_path} ({len(schema)} bytes)")
        return True
    except sqlite3.Error as exc:
        logger.warning(f"Failed to dump schema for {db_path}: {exc}")
        return False


def get_current_date() -> str:
    """Get the current UTC date as YYYY-MM-DD string."""
    return datetime.now(UTC).strftime("%Y-%m-%d")
