#!/usr/bin/env python3
"""
iPod Device Handler Module for iPod Nano Transfer Tool
Handles device detection, mounting, and file transfer to iPod Nano
"""

import os
import sys
import json
import logging
import platform
import shutil
import subprocess
from typing import List, Dict, Optional, Tuple, Any
from pathlib import Path

# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)


class IPodDevice:
    """Class to handle iPod device detection and file transfer"""
    
    def __init__(self, mount_point: Optional[str] = None):
        """
        Initialize the iPod device handler
        
        Args:
            mount_point: Path to iPod mount point (optional)
        """
        self.mount_point = mount_point
        self.system = platform.system()
        self.device_info = {}
        self._block_device = None  # /dev/sdd1 etc.
        self._parent_drive = None  # /dev/sdd (for power-off)
        self.logger = logger
    
    def detect_devices(self) -> List[Dict[str, str]]:
        """
        Detect connected iPod devices
        
        Returns:
            List of dictionaries with device information
        """
        devices = []
        system = platform.system()
        
        try:
            if system == "Linux":
                devices = self._detect_linux()
            elif system == "Darwin":
                devices = self._detect_macos()
            elif system == "Windows":
                devices = self._detect_windows()
        except Exception as e:
            self.logger.error(f"Error detecting devices: {e}")
            
        # If no devices found, check for manual mount point
        if not devices and self.mount_point and os.path.exists(self.mount_point):
            devices.append({
                "id": "manual",
                "name": os.path.basename(self.mount_point),
                "model": "Unknown",
                "mount_point": self.mount_point,
                "mounted": True,
            })
            
        return devices
    
    def _detect_linux(self) -> List[Dict[str, str]]:
        """Detect iPod on Linux using lsblk and udisksctl"""
        devices = []
        
        # Method 1: lsblk JSON output - find devices with IPOD label
        try:
            result = subprocess.run(
                ["lsblk", "-o", "NAME,LABEL,MOUNTPOINT,TYPE", "-J"],
                capture_output=True, check=False, text=True
            )
            
            if result.returncode == 0:
                data = json.loads(result.stdout)
                for device in data.get("blockdevices", []):
                    if device.get("type") != "disk":
                        continue
                    parent_name = device.get("name", "")
                    for child in device.get("children", []):
                        label = child.get("label", "")
                        mountpoint = child.get("mountpoint")
                        child_name = child.get("name", "")
                        
                        if label and "ipod" in label.lower():
                            dev_path = f"/dev/{child_name}"
                            parent_dev = f"/dev/{parent_name}"
                            
                            device_entry = {
                                "id": dev_path,
                                "name": label,
                                "model": "iPod Nano 7",
                                "mount_point": mountpoint,
                                "mounted": mountpoint is not None,
                                "_parent_drive": parent_dev,
                            }
                            devices.append(device_entry)
        except (FileNotFoundError, json.JSONDecodeError) as e:
            self.logger.warning(f"lsblk JSON failed: {e}")
        
        # Method 2: udisksctl dump - find iPod drives
        if not devices:
            try:
                result = subprocess.run(
                    ["udisksctl", "dump"],
                    capture_output=True, check=False, text=True
                )
                
                if result.returncode == 0:
                    current_block = None
                    current_drive = None
                    current_label = None
                    current_mount = None
                    
                    for line in result.stdout.splitlines():
                        if "/org/freedesktop/UDisks2/block_devices/" in line:
                            # Save previous if it was an iPod
                            if current_block and current_label and "ipod" in current_label.lower():
                                devices.append({
                                    "id": current_block,
                                    "name": current_label,
                                    "model": "iPod",
                                    "mount_point": current_mount if current_mount else None,
                                    "mounted": current_mount is not None,
                                    "_parent_drive": self._get_parent_drive_from_udisks(current_block),
                                })
                            current_block = line.split("/block_devices/")[-1].strip().rstrip(":")
                            current_drive = None
                            current_label = None
                            current_mount = None
                        
                        if "IdLabel:" in line and current_block:
                            val = line.split(":", 1)[1].strip()
                            if val and val != "''":
                                current_label = val.strip("'")
                        if "MountPoints:" in line and current_block:
                            val = line.split(":", 1)[1].strip()
                            if val and val != "''" and val != "b''":
                                # MountPoints is a byte array like b'/run/media/mat/IPOD'
                                import ast
                                try:
                                    decoded = ast.literal_eval(val)
                                    if isinstance(decoded, bytes):
                                        current_mount = decoded.decode("utf-8", errors="replace")
                                    elif isinstance(decoded, str):
                                        current_mount = decoded
                                except (ValueError, SyntaxError):
                                    current_mount = val.strip("'\"")
                    
                    # Don't forget the last entry
                    if current_block and current_label and "ipod" in current_label.lower():
                        devices.append({
                            "id": current_block,
                            "name": current_label,
                            "model": "iPod",
                            "mount_point": current_mount if current_mount else None,
                            "mounted": current_mount is not None,
                            "_parent_drive": self._get_parent_drive_from_udisks(current_block),
                        })
            except FileNotFoundError:
                self.logger.warning("udisksctl not found")
        
        # Method 3: Check common mount directories for already-mounted iPods
        for device in devices:
            if not device["mounted"]:
                for base in ["/run/media", "/media", "/mnt"]:
                    if os.path.exists(base):
                        for user_or_device in os.listdir(base):
                            user_path = os.path.join(base, user_or_device)
                            if os.path.isdir(user_path):
                                # Could be /run/media/mat/IPOD or /media/IPOD
                                check_paths = [user_path]
                                if os.path.isdir(user_path):
                                    for sub in os.listdir(user_path):
                                        sub_path = os.path.join(user_path, sub)
                                        if "ipod" in sub.lower() and os.path.isdir(sub_path):
                                            check_paths.append(sub_path)
                                
                                for check_path in check_paths:
                                    if os.path.exists(os.path.join(check_path, "iPod_Control")):
                                        device["mount_point"] = check_path
                                        device["mounted"] = True
                                        break
                            if device["mounted"]:
                                break
                    if device["mounted"]:
                        break
        
        return devices
    
    def _get_parent_drive_from_udisks(self, block_name: str) -> Optional[str]:
        """Get parent drive device from udisksctl for a partition"""
        try:
            # /dev/sdd1 -> /dev/sdd
            if block_name and block_name.startswith("/dev/"):
                name = block_name[5:]  # sdd1
                # Strip trailing partition number
                base = name.rstrip("0123456789")
                if base:
                    return f"/dev/{base}"
        except Exception:
            pass
        return None
    
    def _detect_macos(self) -> List[Dict[str, str]]:
        """Detect iPod on macOS"""
        devices = []
        
        # Check /Volumes for iPod
        volumes_dir = "/Volumes"
        if os.path.exists(volumes_dir):
            for volume in os.listdir(volumes_dir):
                volume_path = os.path.join(volumes_dir, volume)
                if "iPod" in volume and os.path.isdir(volume_path):
                    if os.path.exists(os.path.join(volume_path, "iPod_Control")):
                        devices.append({
                            "id": "unknown",
                            "name": volume,
                            "model": "Unknown",
                            "mount_point": volume_path,
                            "mounted": True,
                        })
        
        return devices
    
    def _detect_windows(self) -> List[Dict[str, str]]:
        """Detect iPod on Windows"""
        devices = []
        import string
        for letter in string.ascii_uppercase:
            drive = f"{letter}:\\"
            if os.path.exists(drive):
                if os.path.exists(os.path.join(drive, "iPod_Control")):
                    devices.append({
                        "id": letter,
                        "name": "iPod",
                        "model": "Unknown",
                        "mount_point": drive,
                        "mounted": True,
                    })
        return devices
    
    def mount_device(self, device_id: Optional[str] = None, detected_mount: Optional[str] = None) -> Optional[str]:
        """
        Mount iPod device using udisksctl (Linux) or verify existing mount
        
        Args:
            device_id: Device path like /dev/sdd1
            detected_mount: Already detected mount point
            
        Returns:
            Mount point if successful, None otherwise
        """
        # If device is already mounted at detected path, just use it
        if detected_mount and os.path.exists(detected_mount):
            if os.path.exists(os.path.join(detected_mount, "iPod_Control")):
                self.mount_point = detected_mount
                self.logger.info(f"Using already mounted device at: {detected_mount}")
                return detected_mount
        
        if self.mount_point and os.path.exists(self.mount_point):
            self.logger.info(f"Device already mounted at: {self.mount_point}")
            return self.mount_point
        
        try:
            if self.system == "Linux":
                return self._mount_linux(device_id)
            elif self.system == "Darwin":
                return self._mount_macos()
            elif self.system == "Windows":
                return self._mount_windows()
        except subprocess.SubprocessError as e:
            self.logger.error(f"Failed to mount device: {e}")
            return None
        
        return None
    
    def _mount_linux(self, device_id: Optional[str]) -> Optional[str]:
        """Mount on Linux using udisksctl"""
        # device_id should be like /dev/sdd1
        if not device_id or not device_id.startswith("/dev/"):
            self.logger.error("No valid device path provided for mounting")
            return None
        
        if not os.path.exists(device_id):
            self.logger.error(f"Device {device_id} does not exist")
            return None
        
        # Check if already mounted via findmnt (avoids udisksctl AlreadyMounted error)
        try:
            fm_result = subprocess.run(
                ["findmnt", "-n", "-o", "TARGET", device_id],
                capture_output=True, check=False, text=True
            )
            if fm_result.returncode == 0 and fm_result.stdout.strip():
                mount_point = fm_result.stdout.strip()
                if os.path.exists(mount_point):
                    self.mount_point = mount_point
                    self._block_device = device_id
                    name = device_id[5:]
                    self._parent_drive = f"/dev/{name.rstrip('0123456789')}"
                    self.logger.info(f"Device already mounted at: {mount_point}")
                    return mount_point
        except Exception:
            self.logger.warning("findmnt check failed, falling through to udisksctl")
        
        try:
            result = subprocess.run(
                ["udisksctl", "mount", "-b", device_id],
                capture_output=True, check=False, text=True
            )
            
            if result.returncode != 0:
                self.logger.error(f"udisksctl mount failed: {result.stderr}")
                return None
            
            # Parse mount point from output: "Mounted /dev/sdd1 at /run/media/mat/IPOD.\n"
            output = result.stdout.strip()
            mount_point = None
            for line in output.splitlines():
                if "Mounted" in line and "at" in line:
                    # "Mounted /dev/sdd1 at /run/media/mat/IPOD."
                    parts = line.split(" at ")
                    if len(parts) >= 2:
                        mp = parts[1].strip().rstrip(".")
                        if os.path.exists(mp):
                            mount_point = mp
            
            if not mount_point:
                # Fallback: check /run/media for the device
                for user_dir in os.listdir("/run/media"):
                    user_path = os.path.join("/run/media", user_dir)
                    if os.path.isdir(user_path):
                        for d in os.listdir(user_path):
                            d_path = os.path.join(user_path, d)
                            if os.path.exists(os.path.join(d_path, "iPod_Control")):
                                mount_point = d_path
                                break
                    if mount_point:
                        break
            
            if mount_point:
                self.mount_point = mount_point
                self._block_device = device_id
                # Extract parent drive: /dev/sdd1 -> /dev/sdd
                name = device_id[5:]
                self._parent_drive = f"/dev/{name.rstrip('0123456789')}"
                self.logger.info(f"Device mounted at: {mount_point}")
                return mount_point
            
            self.logger.error("Could not determine mount point")
            return None
            
        except FileNotFoundError:
            self.logger.error("udisksctl not found. Please install udisks2.")
            return None
    
    def _mount_macos(self) -> Optional[str]:
        """Mount on macOS - devices auto-mount, just find the mount point"""
        result = subprocess.run(
            ["diskutil", "list"],
            capture_output=True, check=False, text=True
        )
        
        if result.returncode == 0:
            for line in result.stdout.splitlines():
                if "iPod" in line:
                    df_result = subprocess.run(
                        ["df", "-h"],
                        capture_output=True, check=False, text=True
                    )
                    for df_line in df_result.stdout.splitlines():
                        if "iPod" in df_line:
                            parts = df_line.split()
                            if len(parts) >= 9:
                                mount_point = parts[8]
                                self.mount_point = mount_point
                                return mount_point
        
        return None
    
    def _mount_windows(self) -> Optional[str]:
        """Mount on Windows"""
        self.logger.warning("Windows mounting not fully implemented")
        return None
    
    def unmount_device(self) -> bool:
        """
        Unmount and safely eject iPod device
        
        Returns:
            True if successful, False otherwise
        """
        if not self.mount_point and not self._block_device:
            self.logger.warning("No device mounted")
            return False
        
        try:
            if self.system == "Linux":
                return self._unmount_linux()
            elif self.system == "Darwin":
                return self._unmount_macos()
            elif self.system == "Windows":
                return self._unmount_windows()
        except subprocess.SubprocessError as e:
            self.logger.error(f"Failed to unmount device: {e}")
            return False
        
        return False
    
    def _unmount_linux(self) -> bool:
        """Unmount and power-off on Linux using udisksctl"""
        success = True
        
        # Step 1: Unmount
        if self.mount_point or self._block_device:
            device = self._block_device
            if not device:
                # Try to find the device from mount point
                result = subprocess.run(
                    ["findmnt", "-n", "-o", "SOURCE", self.mount_point],
                    capture_output=True, check=False, text=True
                )
                if result.returncode == 0 and result.stdout.strip():
                    device = result.stdout.strip()
            
            if device:
                result = subprocess.run(
                    ["udisksctl", "unmount", "-b", device],
                    capture_output=True, check=False, text=True
                )
                
                if result.returncode == 0:
                    self.logger.info(f"Device unmounted: {device}")
                else:
                    self.logger.warning(f"Unmount warning: {result.stderr}")
                    # Not a fatal error - device might already be unmounted
        
        # Step 2: Power off the drive (safely remove)
        drive = self._parent_drive
        if not drive and self._block_device:
            name = self._block_device[5:]
            drive = f"/dev/{name.rstrip('0123456789')}"
        
        if drive:
            result = subprocess.run(
                ["udisksctl", "power-off", "-b", drive],
                capture_output=True, check=False, text=True
            )
            
            if result.returncode == 0:
                self.logger.info(f"Device powered off: {drive}")
            else:
                self.logger.warning(f"Power-off warning: {result.stderr}")
                # Not fatal - USB may have been unplugged already
        
        self.mount_point = None
        self._block_device = None
        self._parent_drive = None
        return success
    
    def _unmount_macos(self) -> bool:
        """Eject on macOS"""
        if self.mount_point:
            subprocess.run(["diskutil", "eject", self.mount_point], check=True)
            self.logger.info(f"Device ejected: {self.mount_point}")
            self.mount_point = None
            return True
        return False
    
    def _unmount_windows(self) -> bool:
        """Unmount on Windows"""
        self.logger.warning("Windows unmounting not fully implemented")
        return False
    
    def is_mounted(self) -> bool:
        """Check if iPod is currently mounted"""
        if self.mount_point and os.path.exists(self.mount_point):
            return os.path.exists(os.path.join(self.mount_point, "iPod_Control"))
        return False
    
    def get_device_info(self) -> Dict[str, Any]:
        """
        Get information about the mounted device
        
        Returns:
            Dictionary with device information
        """
        if not self.mount_point or not os.path.exists(self.mount_point):
            self.logger.warning("No device mounted")
            return {}
            
        info = {
            "mount_point": self.mount_point,
            "total_space": 0,
            "free_space": 0,
            "used_space": 0
        }
        
        try:
            # Get disk usage
            usage = shutil.disk_usage(self.mount_point)
            info["total_space"] = usage.total
            info["free_space"] = usage.free
            info["used_space"] = usage.used
            
            # Try to get track count from Nano7 database
            try:
                from ipod_nano7_db import Nano7Database
                nano7_db = Nano7Database(self.mount_point)
                info["track_count"] = nano7_db.get_track_count()
                info["has_nano7_db"] = True
            except Exception:
                info["has_nano7_db"] = False
            
            return info
            
        except Exception as e:
            self.logger.error(f"Failed to get device info: {e}")
            return info
    
    def transfer_file(self, source_file: str, relative_path: Optional[str] = None) -> bool:
        """
        Transfer file to iPod device
        
        Args:
            source_file: Path to source file
            relative_path: Relative path on iPod (optional)
            
        Returns:
            True if successful, False otherwise
        """
        if not self.mount_point or not os.path.exists(self.mount_point):
            self.logger.warning("No device mounted")
            return False
            
        if not os.path.exists(source_file):
            self.logger.warning(f"Source file does not exist: {source_file}")
            return False
            
        try:
            # Try to use Nano7 database for proper track registration
            try:
                from ipod_nano7_db import Nano7Database
                nano7_db = Nano7Database(self.mount_point)
                result = nano7_db.add_track(source_file)
                if result is None:
                    # Track already exists (duplicate)
                    self.logger.info(f"Track already on iPod, skipping: {os.path.basename(source_file)}")
                    return True  # Not an error - track is already there
                self.logger.info(f"Track added to iPod database: {result['title']}")
                return True
            except ImportError:
                self.logger.warning("ipod_nano7_db not available, using simple file copy")
            except Exception as e:
                self.logger.error(f"Nano7 database transfer failed: {e}")
                return False  # Don't fall back — file was already copied to F00/
            
            # Fallback: simple file copy — only on ImportError
            if relative_path:
                dest_dir = os.path.join(self.mount_point, relative_path)
            else:
                dest_dir = os.path.join(self.mount_point, "Music")
                parts = Path(source_file).parts
                if len(parts) >= 3:
                    artist = parts[-3]
                    album = parts[-2]
                    dest_dir = os.path.join(self.mount_point, "Music", artist, album)
            
            os.makedirs(dest_dir, exist_ok=True)
            filename = os.path.basename(source_file)
            dest_file = os.path.join(dest_dir, filename)
            
            shutil.copy2(source_file, dest_file)
            self.logger.info(f"File transferred: {dest_file}")
            return True
            
        except Exception as e:
            self.logger.error(f"Failed to transfer file: {e}")
            return False
    
    def transfer_directory(self, source_dir: str, dest_dir: Optional[str] = None) -> bool:
        """
        Transfer directory to iPod device
        
        Args:
            source_dir: Path to source directory
            dest_dir: Destination directory on iPod (optional)
            
        Returns:
            True if successful, False otherwise
        """
        if not self.mount_point or not os.path.exists(self.mount_point):
            self.logger.warning("No device mounted")
            return False
            
        if not os.path.exists(source_dir) or not os.path.isdir(source_dir):
            self.logger.warning(f"Source directory does not exist: {source_dir}")
            return False
            
        try:
            # Determine destination path
            if dest_dir:
                full_dest_dir = os.path.join(self.mount_point, dest_dir)
            else:
                full_dest_dir = os.path.join(self.mount_point, "Music")
            
            os.makedirs(full_dest_dir, exist_ok=True)
            
            for root, dirs, files in os.walk(source_dir):
                rel_path = os.path.relpath(root, source_dir)
                if rel_path == '.':
                    rel_path = ''
                
                dest_subdir = os.path.join(full_dest_dir, rel_path)
                os.makedirs(dest_subdir, exist_ok=True)
                
                for file in files:
                    if file.lower().endswith(('.m4a', '.mp3', '.aac', '.mp4')):
                        src_file = os.path.join(root, file)
                        dst_file = os.path.join(dest_subdir, file)
                        shutil.copy2(src_file, dst_file)
                        self.logger.info(f"Transferred: {dst_file}")
            
            self.logger.info(f"Directory transferred: {source_dir} -> {full_dest_dir}")
            return True
            
        except Exception as e:
            self.logger.error(f"Failed to transfer directory: {e}")
            return False

    def export_track(self, track_info: dict, dest_dir: str) -> Optional[str]:
        """
        Copy a track from iPod to a local directory with metadata-based naming.

        Args:
            track_info: Track dict with keys: file_path, title, artist, album, track_number
            dest_dir: Local destination directory

        Returns:
            Path to the exported file, or None on failure
        """
        src_path = track_info.get("file_path")
        if not src_path or not os.path.exists(src_path):
            self.logger.warning(f"Track file not found on iPod: {track_info.get('title')}")
            return None

        if not os.path.exists(dest_dir):
            os.makedirs(dest_dir, exist_ok=True)

        artist = track_info.get("artist", "Unknown Artist")
        title = track_info.get("title", "Unknown Title")
        album = track_info.get("album", "")
        track_num = track_info.get("track_number", 0)
        ext = os.path.splitext(src_path)[1].lower()

        safe_artist = "".join(c for c in artist if c.isalnum() or c in " ._-'&()").strip() or "Unknown"
        safe_title = "".join(c for c in title if c.isalnum() or c in " ._-'&()").strip() or "Unknown"

        if track_num:
            fname = f"{track_num:02d} - {safe_artist} - {safe_title}{ext}"
        else:
            fname = f"{safe_artist} - {safe_title}{ext}"

        dest_path = os.path.join(dest_dir, fname)

        try:
            shutil.copy2(src_path, dest_path)
            self.logger.info(f"Exported track to: {dest_path}")
            return dest_path
        except Exception as e:
            self.logger.error(f"Failed to export track: {e}")
            return None

    def export_tracks(self, tracks: List[dict], dest_dir: str,
                      progress_callback=None) -> List[Tuple[str, str]]:
        """
        Copy multiple tracks from iPod to a local directory.

        Args:
            tracks: List of track info dicts
            dest_dir: Local destination directory
            progress_callback: Optional callback(progress_int, status_str)

        Returns:
            List of (title, exported_path) tuples for successful exports
        """
        exported = []
        total = len(tracks)

        for i, track in enumerate(tracks):
            if progress_callback:
                progress_callback(int((i / total) * 100), f"Exporting {track.get('title', '?')}...")

            path = self.export_track(track, dest_dir)
            if path:
                exported.append((track.get("title", "?"), path))

        if progress_callback:
            progress_callback(100, f"Exported {len(exported)} of {total} tracks")

        return exported


if __name__ == "__main__":
    ipod = IPodDevice()
    
    # Detect devices
    devices = ipod.detect_devices()
    for device in devices:
        status = "mounted" if device.get("mounted") else "NOT mounted"
        print(f"Found device: {device['name']} ({device['model']}) - {status}")
        if device.get("mount_point"):
            print(f"  Mount point: {device['mount_point']}")
        print(f"  Device: {device['id']}")
    
    # Mount and eject if device found
    if devices:
        device = devices[0]
        if not device.get("mounted"):
            mount_point = ipod.mount_device(device["id"])
            if mount_point:
                print(f"Mounted at: {mount_point}")
                
                info = ipod.get_device_info()
                print(f"Free space: {info.get('free_space', 0) / 1024**2:.1f} MB")
                
                # Unmount
                ipod.unmount_device()
                print("Ejected.")
        else:
            print(f"Already mounted at: {device['mount_point']}")
