"""
Numbered Folder Management Module

Manages the numbered folder structure for video deployment. Videos are organized
into numbered subdirectories (001, 002, 003, ...) with a configurable maximum
number of videos per folder (default: 25).

IMPORTANT: A folder must never contain exactly 1 video, because Infuse (media player)
displays a single-video folder as the video itself instead of as a browsable folder.
This means when a new folder is created, it always starts with at least 2 videos.
"""

import os
import re
import json
import shutil
import subprocess
from datetime import datetime
from typing import List, Tuple, Optional, Dict, Any

from .config import CONFIG

VIDEO_EXTENSIONS = {'.m4v', '.mp4'}
FOLDER_IMAGE_NAMES = {'folder.png', 'folder.jpg', 'folder.jpeg'}


def _is_local_path(path: str) -> bool:
    """Check if a path is a local filesystem path (not an rclone remote)."""
    return path.startswith("/") or path.startswith("~")


def _scan_local_files(media_root: str) -> List[Dict[str, Any]]:
    """
    Scan a local directory recursively and return file metadata
    in the same format as rclone lsjson.
    """
    all_files = []
    for dirpath, _dirnames, filenames in os.walk(media_root):
        for name in filenames:
            full_path = os.path.join(dirpath, name)
            rel_path = os.path.relpath(full_path, media_root)
            try:
                mtime = os.path.getmtime(full_path)
                mod_time = datetime.fromtimestamp(mtime).isoformat()
            except OSError:
                mod_time = ""
            all_files.append({
                "Path": rel_path,
                "Name": name,
                "ModTime": mod_time,
                "IsDir": False,
            })
    return all_files


def _move_local_file(source: str, target: str) -> bool:
    """Move a local file, creating target directory if needed."""
    try:
        os.makedirs(os.path.dirname(target), exist_ok=True)
        shutil.move(source, target)
        return True
    except OSError as e:
        print(f"   ⚠️  Fehler beim Verschieben: {e}")
        return False


def _is_video_file(filename: str) -> bool:
    """Check if a filename is a video file based on extension."""
    return any(filename.lower().endswith(ext) for ext in VIDEO_EXTENSIONS)


def _is_folder_image(filename: str) -> bool:
    """Check if a filename is a folder image (e.g., folder.png)."""
    return filename.lower() in FOLDER_IMAGE_NAMES


def _find_sidecars_for_video(video_name: str, all_sidecar_paths: Dict[str, List[str]]) -> List[str]:
    """
    Find all sidecar files belonging to a video.

    Sidecars are non-video files whose name starts with the video stem
    (e.g., "2025-03-02_youtube_abc-fanart.jpg" for "2025-03-02_youtube_abc.m4v").

    Args:
        video_name: The video filename (e.g., "video.m4v")
        all_sidecar_paths: Map of video_stem -> list of sidecar relative paths

    Returns:
        List of sidecar relative paths
    """
    stem = os.path.splitext(video_name)[0]
    return all_sidecar_paths.get(stem, [])


def _run_rclone(args: List[str], timeout: int = 30) -> Optional[str]:
    """Run an rclone command and return stdout, or None on failure."""
    cmd = ["rclone"] + args
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, check=True, timeout=timeout)
        return result.stdout
    except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
        print(f"   ⚠️  rclone Fehler: {e}")
        return None


def get_next_folder_name(current_highest: str) -> str:
    """Return the next numbered folder name (zero-padded to at least 3 digits)."""
    return str(int(current_highest) + 1).zfill(3)


def list_numbered_folders(media_root: str) -> List[Tuple[str, int]]:
    """
    List all numbered folders and their video counts in a media root.

    Args:
        media_root: rclone path to the media root (e.g., "remote:/path/to/category")

    Returns:
        Sorted list of (folder_name, video_count) tuples
    """
    output = _run_rclone(["lsf", media_root, "--dirs-only"])
    if output is None:
        return []

    folders = []
    pattern = re.compile(r'^\d{3,}$')

    for line in output.strip().splitlines():
        folder_name = line.strip().rstrip('/')
        if not folder_name or not pattern.match(folder_name):
            continue

        # Count video files in this folder
        folder_output = _run_rclone(["lsf", f"{media_root}/{folder_name}", "--files-only"])
        if folder_output is None:
            continue

        video_count = sum(
            1 for f in folder_output.strip().splitlines()
            if f.strip() and _is_video_file(f.strip())
        )
        folders.append((folder_name, video_count))

    folders.sort(key=lambda x: int(x[0]))
    return folders


def _find_newest_video_in_folder(media_root: str, folder_name: str) -> Optional[str]:
    """
    Find the video file with the most recent modification date in a folder.

    Returns:
        The filename of the newest video, or None if no videos found
    """
    output = _run_rclone(["lsjson", f"{media_root}/{folder_name}"])
    if output is None:
        return None

    try:
        files = json.loads(output)
    except json.JSONDecodeError:
        return None

    videos = [
        f for f in files
        if not f.get("IsDir", False) and _is_video_file(f.get("Name", ""))
    ]

    if not videos:
        return None

    # Sort by ModTime descending, return newest
    videos.sort(key=lambda f: f.get("ModTime", ""), reverse=True)
    return videos[0]["Name"]


def move_file_with_sidecar(media_root: str, source_folder: str, target_folder: str, filename: str) -> bool:
    """
    Move a video file and its fanart sidecar from one folder to another.

    Args:
        media_root: rclone path to the media root
        source_folder: Source folder name (e.g., "003")
        target_folder: Target folder name (e.g., "004")
        filename: Video filename to move

    Returns:
        True if successful, False otherwise
    """
    source_path = f"{media_root}/{source_folder}/{filename}"
    target_path = f"{media_root}/{target_folder}/{filename}"

    print(f"   📦 Verschiebe {filename}: {source_folder}/ → {target_folder}/")

    result = _run_rclone(["moveto", source_path, target_path, "--progress"])
    if result is None:
        return False

    # Move fanart sidecar if present
    stem = os.path.splitext(filename)[0]
    fanart_name = f"{stem}-fanart.jpg"
    fanart_source = f"{media_root}/{source_folder}/{fanart_name}"
    fanart_target = f"{media_root}/{target_folder}/{fanart_name}"

    # Check if fanart exists (lsf returns empty if not found)
    fanart_check = _run_rclone(["lsf", fanart_source])
    if fanart_check and fanart_check.strip():
        print(f"   🖼️  Verschiebe Fanart: {fanart_name}")
        _run_rclone(["moveto", fanart_source, fanart_target])

    return True


def determine_deploy_folder(media_root: str, max_per_folder: int) -> Tuple[str, Optional[str]]:
    """
    Determine which numbered folder a new video should be deployed to.

    Handles the "never exactly 1 video" rule: when a folder reaches max+1 and
    a new video arrives, the newest video from the full folder is moved to a
    new folder together with the incoming video.

    Args:
        media_root: rclone path to the media root
        max_per_folder: Maximum videos per folder (default: 25)

    Returns:
        Tuple of (target_folder_name, moved_file_new_path_or_None)
        - target_folder_name: The folder to deploy the new video to
        - moved_file_new_path: Full rclone path if a file was moved, None otherwise
    """
    folders = list_numbered_folders(media_root)

    if not folders:
        # No folders exist yet — first video goes to 001
        return ("001", None)

    latest_folder, count = folders[-1]

    if count < max_per_folder:
        # Room available in the latest folder
        return (latest_folder, None)

    if count == max_per_folder:
        # Folder is at capacity — temporarily allow max+1 (OK per spec)
        return (latest_folder, None)

    # count >= max_per_folder + 1: Need to create a new folder
    # Move the newest video from the full folder to the new folder
    new_folder = get_next_folder_name(latest_folder)

    newest_video = _find_newest_video_in_folder(media_root, latest_folder)
    moved_path = None

    if newest_video:
        success = move_file_with_sidecar(media_root, latest_folder, new_folder, newest_video)
        if success:
            moved_path = f"{media_root}/{new_folder}/{newest_video}"

    return (new_folder, moved_path)


def migrate_to_numbered_folders(media_root: str, max_per_folder: int, dry_run: bool = False) -> bool:
    """
    Migrate an existing folder structure (channel-based) to numbered folders.

    Videos are sorted by modification date (oldest first) and distributed into
    numbered folders. The last folder never contains exactly 1 video.

    Args:
        media_root: rclone path to the media root
        max_per_folder: Maximum videos per folder
        dry_run: If True, only show what would happen without making changes

    Returns:
        True if successful, False otherwise
    """
    print(f"\n{'🔍 DRY RUN — ' if dry_run else ''}📁 Migration: {media_root}")
    print(f"   Max. Videos pro Ordner: {max_per_folder}")

    local = _is_local_path(media_root)

    # List all files recursively with metadata
    if local:
        if not os.path.isdir(media_root):
            print(f"   ❌ Verzeichnis nicht gefunden: {media_root}")
            return False
        all_files = _scan_local_files(media_root)
    else:
        output = _run_rclone(["lsjson", media_root, "-R", "--files-only"], timeout=120)
        if output is None:
            print("   ❌ Konnte Dateien nicht auflisten")
            return False
        try:
            all_files = json.loads(output)
        except json.JSONDecodeError:
            print("   ❌ Fehler beim Parsen der Dateiliste")
            return False

    # Separate videos, sidecars, and folder images
    videos = []
    sidecars = {}  # Map: video_stem -> list of sidecar relative paths
    folder_images = []  # List of (path, name) tuples for folder images

    for f in all_files:
        if f.get("IsDir", False):
            continue
        path = f.get("Path", "")
        name = f.get("Name", "")

        if _is_video_file(name):
            videos.append({
                "path": path,
                "name": name,
                "mod_time": f.get("ModTime", ""),
            })
        elif _is_folder_image(name):
            folder_images.append({"path": path, "name": name})
        else:
            # Check if this file is a sidecar for a video (name starts with a video stem)
            # Common patterns: {stem}-fanart.jpg, {stem}-poster.jpg, etc.
            for ext in VIDEO_EXTENSIONS:
                # Find potential video stem by checking if any video stem matches
                pass
            # We'll match sidecars to videos by stem prefix after collecting all files

    # Build sidecar map: for each non-video, non-folder-image file, check if its name
    # starts with any video's stem
    video_stems = set()
    for v in videos:
        video_stems.add(os.path.splitext(v["name"])[0])

    # Re-scan for sidecars
    for f in all_files:
        if f.get("IsDir", False):
            continue
        path = f.get("Path", "")
        name = f.get("Name", "")

        if _is_video_file(name) or _is_folder_image(name):
            continue

        # Check if this file's name starts with any video stem
        for stem in video_stems:
            if name.startswith(stem):
                if stem not in sidecars:
                    sidecars[stem] = []
                sidecars[stem].append(path)
                break

    if not videos:
        print("   ⚠️  Keine Videos gefunden")
        return True

    # Check if already migrated (all videos already in numbered folders)
    numbered_pattern = re.compile(r'^\d{3,}/')
    already_numbered = all(numbered_pattern.match(v["path"]) for v in videos)
    if already_numbered:
        print("   ✅ Alle Videos sind bereits in nummerierten Ordnern")
        return True

    # Sort by modification time (oldest first)
    videos.sort(key=lambda v: v["mod_time"])

    sidecar_count = sum(len(v) for v in sidecars.values())
    print(f"   📊 {len(videos)} Videos gefunden, {sidecar_count} Sidecars, {len(folder_images)} Ordnerbilder")

    # Create batches
    batches = []
    for i in range(0, len(videos), max_per_folder):
        batch = videos[i:i + max_per_folder]
        batches.append(batch)

    # Last-batch rule: never exactly 1 video alone
    if len(batches) > 1 and len(batches[-1]) == 1:
        # Move the single video to the previous batch
        single_video = batches[-1][0]
        batches[-2].append(single_video)
        batches.pop()

    # Show plan
    print(f"\n   📋 Plan: {len(batches)} Ordner")
    for i, batch in enumerate(batches):
        folder_name = str(i + 1).zfill(3)
        print(f"      {folder_name}/ — {len(batch)} Videos")
        if dry_run:
            for v in batch[:3]:
                print(f"         {v['path']}  →  {folder_name}/{v['name']}")
            if len(batch) > 3:
                print(f"         ... und {len(batch) - 3} weitere")

    if dry_run:
        if folder_images:
            print(f"\n      Ordnerbilder → _folder_images/ ({len(folder_images)} Dateien)")
        print(f"\n   🔍 DRY RUN abgeschlossen — keine Änderungen vorgenommen")
        return True

    # Execute migration
    print(f"\n   🚀 Starte Migration...")
    moved_count = 0
    error_count = 0

    for i, batch in enumerate(batches):
        folder_name = str(i + 1).zfill(3)
        print(f"\n   📁 Ordner {folder_name}/ ({len(batch)} Videos)")

        for video in batch:
            source_path = os.path.join(media_root, video['path']) if local else f"{media_root}/{video['path']}"
            target_path = os.path.join(media_root, folder_name, video['name']) if local else f"{media_root}/{folder_name}/{video['name']}"

            # Skip if already in correct folder
            expected_rel = os.path.join(folder_name, video['name']) if local else f"{folder_name}/{video['name']}"
            if video['path'] == expected_rel:
                moved_count += 1
                continue

            if local:
                success = _move_local_file(source_path, target_path)
            else:
                success = _run_rclone(["moveto", source_path, target_path]) is not None

            if success:
                moved_count += 1

                # Move all sidecars belonging to this video
                stem = os.path.splitext(video['name'])[0]
                for sidecar_path in sidecars.get(stem, []):
                    sidecar_name = os.path.basename(sidecar_path)
                    if local:
                        sidecar_source = os.path.join(media_root, sidecar_path)
                        sidecar_target = os.path.join(media_root, folder_name, sidecar_name)
                        _move_local_file(sidecar_source, sidecar_target)
                    else:
                        sidecar_source = f"{media_root}/{sidecar_path}"
                        sidecar_target = f"{media_root}/{folder_name}/{sidecar_name}"
                        _run_rclone(["moveto", sidecar_source, sidecar_target])
            else:
                error_count += 1
                print(f"      ❌ Fehler bei: {video['name']}")

    # Move folder images to _folder_images/
    if folder_images:
        print(f"\n   🖼️  Verschiebe {len(folder_images)} Ordnerbilder nach _folder_images/")
        for img in folder_images:
            if local:
                source = os.path.join(media_root, img['path'])
                target = os.path.join(media_root, "_folder_images", img['path'])
                _move_local_file(source, target)
            else:
                source = f"{media_root}/{img['path']}"
                target = f"{media_root}/_folder_images/{img['path']}"
                _run_rclone(["moveto", source, target])

    # Clean up empty directories
    print(f"\n   🧹 Räume leere Ordner auf...")
    if local:
        # Remove empty directories bottom-up
        for dirpath, dirnames, filenames in os.walk(media_root, topdown=False):
            if dirpath == media_root:
                continue
            # Skip numbered folders and _folder_images
            if not filenames and not dirnames:
                try:
                    os.rmdir(dirpath)
                except OSError:
                    pass
    else:
        _run_rclone(["rmdirs", media_root], timeout=60)

    print(f"\n   ✅ Migration abgeschlossen: {moved_count} Videos verschoben, {error_count} Fehler")
    return error_count == 0


def interactive_migration() -> None:
    """
    Interactive menu for migrating a folder structure to numbered folders (Laufnummern-Verzeichnisse).
    The user provides a root directory path, and all videos within are reorganized.
    """
    print("\n" + "=" * 80)
    print("📁 ORDNERSTRUKTUR MIGRIEREN → LAUFNUMMERN-VERZEICHNISSE")
    print("=" * 80)

    max_per_folder = CONFIG.get("max_videos_per_folder", 25)
    print(f"\nMax. Videos pro Ordner: {max_per_folder}")
    print("Gib das Root-Verzeichnis an, dessen Inhalte in Laufnummern-Verzeichnisse")
    print("(001, 002, ...) reorganisiert werden sollen.")
    print()
    print("Beispiele:")
    print("  lyssach-nas:/Emby/media/Internetfilme/Patrick")
    print("  /Volumes/Media/Internetfilme/Allgemein")
    print()

    root_path = input("Root-Verzeichnis (oder 'z' für Zurück): ").strip().strip('"').strip("'")

    if not root_path or root_path.lower() == 'z':
        return

    # Dry run first
    print("\n--- DRY RUN ---")
    migrate_to_numbered_folders(root_path, max_per_folder, dry_run=True)

    # Confirm
    confirm = input("\nMigration durchführen? [y/N]: ").strip().lower()
    if confirm not in ['y', 'j', 'ja', 'yes']:
        print("   Abgebrochen.")
        return

    # Execute
    print("\n--- MIGRATION ---")
    success = migrate_to_numbered_folders(root_path, max_per_folder, dry_run=False)

    if success:
        print("\n✅ Migration erfolgreich abgeschlossen!")
        reindex = input("\nMetadata-Index neu erstellen? [Y/n]: ").strip().lower()
        if reindex in ['', 'y', 'j', 'ja', 'yes']:
            from .module_index import index_server_videos
            index_server_videos()
    else:
        print("\n⚠️  Migration mit Fehlern abgeschlossen. Bitte prüfen.")
