"""Operations that drive mounting, unmounting, and diagnostics for externals."""

from __future__ import annotations

import sys

from pathlib import Path
from typing import List, Tuple

from .config import ExternalsConfig, project_root
from .registry import load_registry, resolve_registered_path
from .utils import ensure_dir, have, maybe_sudo, on_ntfs, resolve_source, run


def is_mounted(target: Path) -> bool:
    """
    Detect whether the provided target path is already a mount point.

    Args:
        target: Path that represents the expected mount point.

    Returns:
        True if `findmnt` or `/proc/self/mountinfo` reports the target as mounted.
    """
    try:
        if have("findmnt"):
            result = run(["findmnt", "-n", "-T", str(target)], check=False)
            return result.returncode == 0 and result.stdout.strip() != ""
        mountinfo = Path("/proc/self/mountinfo").read_text()
        return any((" " + str(target) + " ") in line for line in mountinfo.splitlines())
    except Exception:
        return False


def mount_source_of(target: Path) -> str | None:
    """
    Determine the source device of a mounted target.

    Args:
        target: Mount point whose source path should be retrieved.

    Returns:
        Absolute path string of the mount source, or None if the target is not mounted.
    """
    try:
        if have("findmnt"):
            result = run(
                ["findmnt", "-n", "-T", str(target), "-o", "SOURCE"], check=False
            )
            if result.returncode == 0:
                source = result.stdout.strip()
                return source if source else None
        mountinfo = Path("/proc/self/mountinfo").read_text().splitlines()
        for line in mountinfo:
            parts = line.split()
            if len(parts) > 4 and parts[4] == str(target):
                if " - " in line:
                    tail = line.split(" - ", 1)[1].split()
                    if len(tail) >= 3:
                        return tail[2]
        return None
    except Exception:
        return None


def bind_mount(src: Path, tgt: Path) -> None:
    """
    Bind-mount the source directory to the target and enforce read-only mode.

    Args:
        src: Path of the external source directory.
        tgt: Destination path under the configured `externalsDir`.

    Raises:
        SystemExit: When either the bind or remount operation fails unexpectedly.
    """
    ensure_dir(tgt)
    cmd1 = maybe_sudo(["mount", "--bind", str(src), str(tgt)])
    res1 = run(cmd1, check=False)
    if res1.returncode != 0 and "already mounted" not in res1.stderr.lower():
        sys.exit(f"[externals] mount --bind failed for {src} -> {tgt}\n{res1.stderr}")
    cmd2 = maybe_sudo(["mount", "-o", "remount,bind,ro", str(tgt)])
    res2 = run(cmd2, check=False)
    if res2.returncode != 0:
        sys.exit(f"[externals] remount ro failed for {tgt}\n{res2.stderr}")


def do_mount(config: ExternalsConfig) -> None:
    """
    Mount (or refresh) every configured external source.

    Args:
        config: Parsed configuration describing each import.
    """
    ensure_dir(config.externals_dir)
    registry = load_registry()
    for entry in config.imports:
        path_str = entry.path or resolve_registered_path(entry.name, registry)
        if path_str is None:
            sys.exit(
                f"[externals] {entry.name!r} is not registered. "
                "Run `externai register` inside that project first."
            )
        tgt = (config.externals_dir / entry.mount).resolve()
        src = resolve_source(path_str)
        if not src.exists():
            print(f"[warn] Source does not exist: {src} (for {entry.name})", file=sys.stderr)
            continue
        ensure_dir(tgt)
        if is_mounted(tgt):
            current = mount_source_of(tgt)
            if current and Path(current).resolve() == src:
                remount = run(
                    maybe_sudo(["mount", "-o", "remount,bind,ro", str(tgt)]), check=False
                )
                if remount.returncode == 0:
                    print(f"[ok] {entry.name}: already mounted RO from {src}")
                else:
                    print(
                        f"[warn] {entry.name}: remount RO failed, leaving as-is\n{remount.stderr}"
                    )
                continue
            run(maybe_sudo(["umount", str(tgt)]), check=False)
        bind_mount(src, tgt)
        print(f"[ok] {entry.name}: {src} -> {tgt} (RO)")


def _cleanup_mount_target(target: Path) -> None:
    """
    Remove an empty mount directory after unmounting.

    We intentionally never delete non-empty directories to avoid data loss.
    """
    if not target.exists():
        return
    try:
        target.rmdir()
    except OSError:
        print(
            f"[warn] mount target not empty, leaving it in place: {target}",
            file=sys.stderr,
        )


def do_unmount(config: ExternalsConfig) -> None:
    """
    Unmount every target defined in the configuration.

    Args:
        config: Parsed configuration describing each import.
    """
    registry = load_registry()
    any_error = False
    for entry in config.imports:
        tgt = (config.externals_dir / entry.mount).resolve()
        if is_mounted(tgt):
            result = run(maybe_sudo(["umount", str(tgt)]), check=False)
            if result.returncode == 0:
                print(f"[ok] unmounted {entry.name}")
                _cleanup_mount_target(tgt)
            else:
                any_error = True
                print(
                    f"[err] failed to unmount {entry.name}: {result.stderr.strip()}",
                    file=sys.stderr,
                )
        else:
            print(f"[..] {entry.name}: not mounted")
    if any_error:
        sys.exit(1)


def do_status(config: ExternalsConfig) -> None:
    """
    Print a status table describing each configured mount.

    Args:
        config: Parsed configuration describing each import.
    """
    rows: List[Tuple[str, str, str, str, str]] = []
    registry = load_registry()
    for entry in config.imports:
        tgt = (config.externals_dir / entry.mount).resolve()
        path_str = entry.path or resolve_registered_path(entry.name, registry)
        src_path = resolve_source(path_str) if path_str else None
        mounted = is_mounted(tgt)
        raw_source = mount_source_of(tgt) if mounted else None
        if raw_source:
            source = raw_source
        elif entry.path is None and path_str is None:
            source = "<unregistered>"
        else:
            source = "-"
        if src_path:
            exists = "yes" if src_path.exists() else "no"
            ntfs = "yes" if (on_ntfs(src_path) or on_ntfs(tgt)) else "no"
        else:
            exists = "n/a"
            ntfs = "n/a"
        rows.append(
            (
                entry.name,
                exists,
                "mounted" if mounted else "no",
                source,
                ntfs,
            )
        )
    print(f"{'name':20} {'src_exists':10} {'mounted':10} {'mount_source':40} {'ntfs-ish?':9}")
    print("-" * 95)
    for name, exists, mounted, source, ntfs in rows:
        truncated = source if len(source) < 40 else source[:37] + "..."
        print(f"{name:20} {exists:10} {mounted:10} {truncated:40} {ntfs:9}")


def do_doctor(config: ExternalsConfig) -> None:
    """
    Run heuristics to surface potential issues for mounting.

    Args:
        config: Parsed configuration describing each import.
    """
    issues = 0
    if not have("mount"):
        issues += 1
        print("[doctor] 'mount' not found in PATH.")
    if not have("umount"):
        issues += 1
        print("[doctor] 'umount' not found in PATH.")
    if not have("findmnt"):
        print("[doctor] 'findmnt' not found; falling back to /proc parsing (OK).")
    registry = load_registry()
    for entry in config.imports:
        path_str = entry.path or resolve_registered_path(entry.name, registry)
        if path_str is None:
            issues += 1
            print(
                f"[doctor] {entry.name}: not registered yet (run `externai register`)."
            )
            continue
        src = resolve_source(path_str)
        if on_ntfs(src):
            issues += 1
            print(
                f"[doctor] WARNING: source for {entry.name} appears on /mnt/* (likely NTFS). RO may be less strict."
            )
    if on_ntfs(project_root()):
        issues += 1
        print(
            "[doctor] Project root appears on /mnt/* (likely NTFS). Prefer WSL Linux filesystem for strict RO."
        )
    ensure_dir(config.externals_dir)
    detail = "" if issues == 0 else f" Found {issues} potential issue(s)."
    print("[doctor] Checks complete." + detail)
