"""Disk scanner module to detect disks and partitions using smartctl."""

import os
import re
from datetime import datetime
from typing import List, Optional, Tuple, Dict
#from p20260130_12_kilo01.models import Disk, Partition
#from p20260130_12_kilo01.shell_calls import (
from jusfltuls.models import Disk, Partition
from jusfltuls.shell_calls import (
    run_command,
    build_sudo_btrfs_device_stats,
    build_sudo_btrfs_scrub_status,
    build_sudo_snapper_c_list,
)


def get_partitions_from_disk_by_uuid() -> List[Dict[str, str]]:
    """Get all partitions from /dev/disk/by-uuid directory.

    Returns:
        List of dictionaries containing partition info:
        - uuid: The UUID of the partition
        - device_name: The device name (e.g., 'sda1', 'nvme0n1p1')
        - device_path: Full device path (e.g., '/dev/sda1')
    """
    partitions = []
    by_uuid_path = "/dev/disk/by-uuid"

    try:
        if not os.path.isdir(by_uuid_path):
            return partitions

        for entry in os.listdir(by_uuid_path):
            entry_path = os.path.join(by_uuid_path, entry)
            if os.path.islink(entry_path):
                # Resolve the symlink to get the actual device
                target = os.readlink(entry_path)
                if not target.startswith("/"):
                    # Relative path, resolve it
                    target = os.path.normpath(os.path.join(by_uuid_path, target))
                else:
                    target = os.path.normpath(target)

                # Extract device name from target (e.g., ../../nvme0n1p1 -> nvme0n1p1)
                device_name = os.path.basename(target)

                partitions.append({
                    "uuid": entry,
                    "device_name": device_name,
                    "device_path": f"/dev/{device_name}"
                })
    except (OSError, IOError):
        pass

    return partitions


def get_disks_from_smartctl_scan() -> List[str]:
    """Get list of available disks using smartctl --scan.

    Returns:
        List of device paths (e.g., ['/dev/sda', '/dev/sdb'])
    """
    cmd = ["smartctl", "--scan"]
    returncode, output = run_command(cmd)
    if returncode != 0:
        return []

    disks = []
    for line in output.strip().split('\n'):
        # Parse lines like: /dev/sda -d scsi # /dev/sda, SCSI device
        match = re.match(r'^(\S+)\s+-d', line)
        if match:
            disks.append(match.group(1))

    return disks


def get_device_size(device: str) -> str:
    """Get device size in human-readable format."""
    # For NVMe devices, convert /dev/nvmeX to /dev/nvmeXn1 for size detection
    size_device = device
    if device.startswith("/dev/nvme"):
        last_part = device.split("/")[-1]
        if re.match(r"nvme\d+$", last_part):  # Matches nvme0, nvme1, etc. (controller)
            size_device = device + "n1"

    # Use -d flag to show only the device itself, not partitions
    cmd = ["lsblk", "-b", "-n", "-d", "-o", "SIZE", size_device]
    returncode, out = run_command(cmd)
    if returncode != 0:
        return "Unknown"

    try:
        size_bytes = int(out.strip())
        # Convert to appropriate unit
        if size_bytes >= 1024**4:
            return f"{size_bytes / (1024**4):.1f}TB"
        elif size_bytes >= 1024**3:
            return f"{size_bytes / (1024**3):.1f}GB"
        elif size_bytes >= 1024**2:
            return f"{size_bytes / (1024**2):.1f}MB"
        else:
            return f"{size_bytes / 1024:.1f}KB"
    except (ValueError, ZeroDivisionError):
        return "Unknown"


def get_filesystem_type(device: str) -> str:
    """Get filesystem type for a device."""
    cmd = ["lsblk", "-n", "-o", "FSTYPE", device]
    returncode, out = run_command(cmd)
    if returncode == 0:
        fs_type = out.strip().split("\n")[0]
        return fs_type if fs_type else "Unknown"
    return "Unknown"


def get_disk_type(device_name: str) -> str:
    """Determine disk type from device name."""
    if "nvme" in device_name.lower():
        return "NVMe"
    elif device_name.startswith("sd"):
        return "SATA"
    elif device_name.startswith("hd"):
        return "IDE"
    elif device_name.startswith("mmc"):
        return "MMC"
    else:
        return "Unknown"


def resolve_symlink(path: str) -> str:
    """Resolve a symlink to its target."""
    try:
        if os.path.islink(path):
            target = os.readlink(path)
            if not target.startswith("/"):
                # Relative path, resolve it
                target = os.path.join(os.path.dirname(path), target)
            return os.path.normpath(target)
        return path
    except OSError:
        return path


def get_partition_size(device_path: str) -> str:
    """Get partition size in human-readable format."""
    cmd = ["lsblk", "-b", "-n", "-o", "SIZE", device_path]
    returncode, out = run_command(cmd)
    if returncode != 0:
        return "Unknown"

    try:
        size_bytes = int(out.strip())
        # Convert to appropriate unit
        if size_bytes >= 1024**4:
            return f"{size_bytes / (1024**4):.1f}TB"
        elif size_bytes >= 1024**3:
            return f"{size_bytes / (1024**3):.1f}GB"
        elif size_bytes >= 1024**2:
            return f"{size_bytes / (1024**2):.1f}MB"
        else:
            return f"{size_bytes / 1024:.1f}KB"
    except (ValueError, ZeroDivisionError):
        return "Unknown"


def get_partition_filesystem(device_path: str) -> str:
    """Get filesystem type for a partition device."""
    cmd = ["lsblk", "-n", "-o", "FSTYPE", device_path]
    returncode, out = run_command(cmd)
    if returncode == 0:
        fs_type = out.strip().split("\n")[0]
        return fs_type if fs_type else "Unknown"
    return "Unknown"


def get_mountpoint_for_partition(device_path: str) -> Optional[str]:
    """Get mount point for a partition device."""
    cmd = ["lsblk", "-n", "-o", "MOUNTPOINT", device_path]
    returncode, out = run_command(cmd)
    if returncode == 0:
        mountpoint = out.strip().split("\n")[0]
        return mountpoint if mountpoint else None
    return None


def get_btrfs_device_stats(device_path: str) -> str:
    """Check btrfs device stats for errors.

    Returns:
        "OK" if no errors, "ERR" if any errors found, "" if not btrfs or error
    """
    cmd = build_sudo_btrfs_device_stats(device_path)
    returncode, output = run_command(cmd)
    if returncode != 0:
        return ""

    # Parse output for error counts
    # Format: [/dev/xxx].write_io_errs    0
    total_errors = 0
    for line in output.strip().split("\n"):
        match = re.search(r"(\d+)\s*$", line)
        if match:
            total_errors += int(match.group(1))

    return "ERR" if total_errors > 0 else "OK"


def get_btrfs_scrub_status(mountpoint: str) -> str:
    """Check btrfs scrub status.

    Returns:
        Percentage string (e.g., "45%") if scrub is running,
        empty string if not running or not btrfs
    """
    cmd = build_sudo_btrfs_scrub_status(mountpoint)
    returncode, output = run_command(cmd)
    if returncode != 0:
        return ""

    # Check if scrub is running
    if "running" in output.lower():
        # Look for "Bytes scrubbed: X (YY%)" line
        # Format: Bytes scrubbed: 391.18GiB  (84.65%)
        for line in output.split("\n"):
            if "bytes scrubbed" in line.lower():
                match = re.search(r"\((\d+(?:\.\d+)?)%\)", line)
                if match:
                    return f"{match.group(1)}%"
        # Fallback: look for any percentage in output
        match = re.search(r"(\d+(?:\.\d+)?)%", output)
        if match:
            return f"{match.group(1)}%"
        return "RUN"

    return ""


def get_btrfs_last_scrub_hours(mountpoint: str) -> Optional[int]:
    """Return hours since the last completed btrfs scrub for a mountpoint.

    Parses `btrfs scrub status` output looking for:
      Scrub started:    Mon Feb 23 13:54:38 2026
      Status:           finished

    Only counts scrubs whose Status is "finished" (not "running" or "aborted").
    Returns None if no completed scrub is found or the time cannot be parsed.
    """
    cmd = build_sudo_btrfs_scrub_status(mountpoint)
    returncode, output = run_command(cmd)
    if returncode != 0:
        return None

    status = ""
    scrub_started: Optional[datetime] = None

    for line in output.split("\n"):
        line = line.strip()
        if line.lower().startswith("status:"):
            status = line.split(":", 1)[1].strip().lower()
        elif line.lower().startswith("scrub started:"):
            date_str = line.split(":", 1)[1].strip()
            # Format: Mon Feb 23 13:54:38 2026
            try:
                scrub_started = datetime.strptime(date_str, "%a %b %d %H:%M:%S %Y")
            except ValueError:
                pass

    if status == "finished" and scrub_started is not None:
        delta = datetime.now() - scrub_started
        return int(delta.total_seconds() / 3600)

    return None


def get_snapper_snapshot_count(config_name: str) -> int:
    """Get the number of snapshots for a snapper config.

    Returns:
        Number of snapshots, or 0 if error
    """
    cmd = build_sudo_snapper_c_list(config_name)
    returncode, output = run_command(cmd)
    if returncode != 0:
        return 0

    # Count non-empty lines excluding header lines
    count = 0
    for line in output.strip().split("\n"):
        line = line.strip()
        # Skip header, separator, and empty lines
        if not line or line.startswith("#") or line.startswith("-") or line.startswith("Type"):
            continue
        count += 1

    return count


def normalize_disk_name(disk_name: str) -> str:
    """Normalize disk name for partition matching.

    smartctl --scan returns nvme0, nvme1 (controllers)
    but actual block devices are nvme0n1, nvme1n1 (namespaces)
    This function converts controller names to namespace names.
    """
    # If it's an NVMe controller name (nvme0, nvme1, etc.) without namespace
    if re.match(r'^nvme\d+$', disk_name):
        # Default to namespace 1 (nvme0 -> nvme0n1)
        return f"{disk_name}n1"
    return disk_name


def get_partitions_for_disk(disk_path: str) -> List[Partition]:
    """Get all partitions for a given disk using /dev/disk/by-uuid."""
    partitions = []
    disk_name = os.path.basename(disk_path)

    # Normalize disk name (handle nvme0 -> nvme0n1 conversion)
    normalized_disk_name = normalize_disk_name(disk_name)

    # Get all partitions from /dev/disk/by-uuid
    all_partitions = get_partitions_from_disk_by_uuid()

    # Get snapper managed devices and mountpoint to config mapping
    from jusfltuls.snapper import (
        get_snapper_managed_devices,
        get_snapper_mountpoint_to_config_map,
    )
    snapper_devices = get_snapper_managed_devices()
    mountpoint_to_config = get_snapper_mountpoint_to_config_map()

    # Filter partitions belonging to this disk
    for part_info in all_partitions:
        device_name = part_info["device_name"]
        device_path = part_info["device_path"]

        # Check if this partition belongs to the current disk
        # For nvme: nvme0n1p1 belongs to nvme0n1
        # For sda: sda1 belongs to sda
        belongs_to_disk = False

        if normalized_disk_name.startswith("nvme"):
            # NVMe devices: nvme0n1p1, nvme0n1p2, etc. belong to nvme0n1
            if device_name.startswith(normalized_disk_name + "p"):
                belongs_to_disk = True
        elif normalized_disk_name.startswith("sd") or normalized_disk_name.startswith("hd"):
            # SATA/IDE devices: sda1, sda2, etc. belong to sda
            if device_name.startswith(normalized_disk_name) and device_name != normalized_disk_name:
                belongs_to_disk = True
        elif normalized_disk_name.startswith("mmc"):
            # MMC devices: mmcblk0p1 belongs to mmcblk0
            if device_name.startswith(normalized_disk_name + "p"):
                belongs_to_disk = True

        if belongs_to_disk:
            size = get_partition_size(device_path)
            filesystem = get_partition_filesystem(device_path)
            mountpoint = get_mountpoint_for_partition(device_path)

            # Check if this partition is managed by snapper
            snapper_managed = device_path in snapper_devices

            # Get snapper config and snapshot count if managed
            snapper_config = None
            snapper_snapshot_count = 0
            if snapper_managed and mountpoint:
                snapper_config = mountpoint_to_config.get(mountpoint)
                if snapper_config:
                    snapper_snapshot_count = get_snapper_snapshot_count(snapper_config)

            # Get btrfs status if this is a btrfs partition
            btrfs_errors = ""
            btrfs_scrub_status = ""
            btrfs_last_scrub_hours = None
            if filesystem == "btrfs":
                btrfs_errors = get_btrfs_device_stats(device_path)
                if mountpoint:
                    btrfs_scrub_status = get_btrfs_scrub_status(mountpoint)
                    btrfs_last_scrub_hours = get_btrfs_last_scrub_hours(mountpoint)

            partition = Partition(
                name=device_name,
                device_path=device_path,
                size=size,
                filesystem=filesystem,
                snapper_managed=snapper_managed,
                snapper_config=snapper_config,
                snapper_snapshot_count=snapper_snapshot_count,
                mountpoint=mountpoint,
                btrfs_errors=btrfs_errors,
                btrfs_scrub_status=btrfs_scrub_status,
                btrfs_last_scrub_hours=btrfs_last_scrub_hours,
            )
            partitions.append(partition)

    # Sort partitions by name for consistent display
    partitions.sort(key=lambda p: p.name)

    return partitions


def scan_disks() -> Tuple[List[Disk], Optional[str]]:
    """Scan for available disks using smartctl --scan."""
    disks = []
    error = None

    # Get disks from smartctl --scan
    disk_paths = get_disks_from_smartctl_scan()

    if not disk_paths:
        return [], "No disks found by smartctl --scan"

    # Create Disk objects from smartctl scan results
    for device_path in disk_paths:
        disk_name = os.path.basename(device_path)
        disk_type = get_disk_type(disk_name)
        size = get_device_size(device_path)
        partitions = get_partitions_for_disk(device_path)

        disk = Disk(
            name=disk_name,
            device_path=device_path,
            size=size,
            disk_type=disk_type,
            partitions=partitions
        )
        disks.append(disk)

    # Sort disks by name
    disks.sort(key=lambda d: d.name)

    return disks, error
