import os
import sys
import shutil
import json
import hashlib
import platform
import subprocess
import time
from pathlib import Path
from datetime import datetime

# --- Defaults ---
DEFAULT_CONFIG = {
    "tv_dir": r"Z:\tv",
    "content_dir": r"Z:\content",
    "target_vmaf": 93.0,
    "min_savings": 12.0,
    "gpu_jobs": 2,
    "cpu_jobs": 1,
    "av1_crf": 34,
    "hevc_crf": 28,
    "temp_dir_windows": r"C:\Users\bnair\Videos\encodes",
    "temp_dir_linux": "~/Videos/encodes"
}

# --- Paths ---
def get_base_paths(args=None):
    """
    Determine root paths for locks and logs.
    Priority:
    1. Shared Network Drive (parent of tv_dir) -> Z:\.vmaf_locks
    2. Local Fallback (current dir / logs)
    """
    tv_dir = Path(args.tv_dir) if args and hasattr(args, 'tv_dir') else Path(DEFAULT_CONFIG["tv_dir"])
    
    # Logic: Locks MUST be at the common root of content to be shared.
    # We attempt to find the common parent if both are supplied, otherwise default to tv_dir parent.
    
    potential_roots = []
    if args:
        if hasattr(args, 'tv_dir') and args.tv_dir: potential_roots.append(Path(args.tv_dir))
        if hasattr(args, 'content_dir') and args.content_dir: potential_roots.append(Path(args.content_dir))
    
    if not potential_roots:
        potential_roots.append(Path(DEFAULT_CONFIG["tv_dir"]))

    # Find common path
    try:
        if len(potential_roots) > 1:
            # If they are on different drives, common_path might fail or return empty on Windows
            # In that case, we fall back to the first one (TV dir parent)
            common_root = os.path.commonpath(potential_roots)
            shared_root = Path(common_root)
            # If common root is the root drive itself (e.g. Z:\), that's fine.
            # If it's a subdir (Z:\Media), also fine.
            # But if it's too deep (e.g. just Z:\), we want to make sure we don't dump .vmaf_locks in root unless intended.
            # Typically, we want the parent of the library folders.
            # If os.path.commonpath returns Z:\Media, and dirs are Z:\Media\TV and Z:\Media\Content
            # Then locks go to Z:\Media\.vmaf_locks. Perfect.
        else:
            shared_root = potential_roots[0].parent
    except:
        # Fallback for different drives
        shared_root = potential_roots[0].parent
    
    # Defaults
    lock_dir = Path("locks").resolve()
    log_dir = Path("logs").resolve()
    
    # Network Logic
    if shared_root.exists():
        network_lock = shared_root / ".vmaf_locks"
        # We prefer local logs to avoid network I/O issues during logging, 
        # but locks MUST be shared.
        try:
            network_lock.mkdir(parents=True, exist_ok=True)
            lock_dir = network_lock
        except Exception as e:
            print(f"[Warning] Could not create network locks at {network_lock}: {e}")
            print(f"          Falling back to local locks: {lock_dir}")

    # Ensure existence
    lock_dir.mkdir(parents=True, exist_ok=True)
    log_dir.mkdir(parents=True, exist_ok=True)
    
    return lock_dir, log_dir

def get_temp_dir(args=None):
    # Check args override
    if args and hasattr(args, 'temp_dir') and args.temp_dir:
        path = Path(args.temp_dir)
    elif platform.system() == "Windows":
        path = Path(DEFAULT_CONFIG["temp_dir_windows"])
    else:
        path = Path(DEFAULT_CONFIG["temp_dir_linux"]).expanduser()
        
    path.mkdir(parents=True, exist_ok=True)
    return path

# --- Logging ---
def log_event(log_dir, filename, data):
    log_path = Path(log_dir) / filename
    data["timestamp"] = datetime.now().isoformat()
    try:
        with open(log_path, "a", encoding="utf-8") as f:
            f.write(json.dumps(data) + "\n")
    except Exception as e:
        print(f"[ERROR] Failed to write log: {e}")

# --- Dependencies ---
def check_dependencies(required_tools=None):
    if required_tools is None:
        required_tools = ["ffmpeg", "ffprobe", "ab-av1"]
        
    missing = []
    # Determine bin path relative to this file
    bin_path = Path(__file__).parent.parent / "bin"
    
    for tool in required_tools:
        if tool == "ab-av1":
            # Check bundled first in bin/
            bundled = bin_path / "ab-av1.exe"
            if bundled.exists(): continue
            # Check PATH
            if shutil.which("ab-av1"): continue
            missing.append("ab-av1")
        else:
            if not shutil.which(tool):
                missing.append(tool)
    
    if missing:
        print(f"[!] CRITICAL: Missing tools: {', '.join(missing)}")
        print(f"    Checked bundled path: {bin_path}")
        sys.exit(1)

# --- Metadata ---
def get_video_info(filepath):
    """Get video info using ffprobe (Standardized)"""
    try:
        cmd = [
            "ffprobe", "-v", "quiet",
            "-print_format", "json",
            "-show_format", "-show_streams",
            str(filepath)
        ]
        result = subprocess.run(cmd, capture_output=True, text=True, check=True)
        data = json.loads(result.stdout)
        
        video = next((s for s in data["streams"] if s["codec_type"] == "video"), None)
        if not video: return None
            
        size = int(data["format"].get("size", 0))
        duration = float(data["format"].get("duration", 0))
        
        # Bitrate calc
        bitrate = int(data["format"].get("bitrate", 0))
        if bitrate == 0 and duration > 0:
            bitrate = int((size * 8) / duration)

        return {
            "codec": video.get("codec_name"),
            "width": int(video.get("width", 0)),
            "height": int(video.get("height", 0)),
            "duration": duration,
            "size": size,
            "bitrate": bitrate,
            "fps": video.get("r_frame_rate", "0/0")
        }
    except Exception:
        return None

# --- Locks ---
def acquire_lock(lock_dir, filepath):
    """
    Simple file-based lock.
    Returns lock_path if acquired, None if failed.
    """
    # Use hash of filename to avoid long paths/invalid chars
    fhash = hashlib.md5(str(filepath.name).encode()).hexdigest()
    lock_file = lock_dir / f"{fhash}.lock"
    
    if lock_file.exists():
        # Check staleness (24h)
        try:
            if time.time() - lock_file.stat().st_mtime > 86400:
                lock_file.unlink()
            else:
                return None
        except:
            return None
            
    try:
        lock_file.touch()
        return lock_file
    except:
        return None

# --- Hardware Detection ---
def detect_hardware_encoder(args=None):
    """Detects available hardware encoders via ffmpeg (Cross-Platform)"""
    
    # Check for forced CPU mode via args
    if args and getattr(args, 'cpu_only', False):
        # We still need to determine WHICH software encoder to use
        # But we skip HW checks.
        try:
            res = subprocess.run(["ffmpeg", "-hide_banner", "-encoders"], capture_output=True, text=True)
            out = res.stdout
            if "libsvtav1" in out: return "libsvtav1", "libx265", "cpu"
            if "libx265" in out: return None, "libx265", "cpu"
            return None, None, "cpu"
        except:
            return None, None, "cpu"

    try:
        # Run ffmpeg -encoders
        res = subprocess.run(["ffmpeg", "-hide_banner", "-encoders"], capture_output=True, text=True)
        out = res.stdout
        
        av1_enc = None
        hevc_enc = None
        hw_type = "cpu"
        
        # 1. AMD (AMF) - Windows (Preferred)
        if "av1_amf" in out: av1_enc = "av1_amf"
        if "hevc_amf" in out: hevc_enc = "hevc_amf"
        if av1_enc or hevc_enc: return av1_enc, hevc_enc, "amf"

        # 2. NVIDIA (NVENC) - Windows/Linux
        if "av1_nvenc" in out: av1_enc = "av1_nvenc"
        if "hevc_nvenc" in out: hevc_enc = "hevc_nvenc"
        if av1_enc or hevc_enc: return av1_enc, hevc_enc, "nvenc"
        
        # 3. AMD (VAAPI) - Linux
        # Often named hevc_vaapi, av1_vaapi
        if "av1_vaapi" in out: av1_enc = "av1_vaapi"
        if "hevc_vaapi" in out: hevc_enc = "hevc_vaapi"
        if av1_enc or hevc_enc: return av1_enc, hevc_enc, "vaapi"
        
        # 4. Intel (QSV) - Windows/Linux
        if "av1_qsv" in out: av1_enc = "av1_qsv"
        if "hevc_qsv" in out: hevc_enc = "hevc_qsv"
        if av1_enc or hevc_enc: return av1_enc, hevc_enc, "qsv"
        
        # 5. Apple Silicon (VideoToolbox) - macOS
        if "av1_videotoolbox" in out: av1_enc = "av1_videotoolbox"
        if "hevc_videotoolbox" in out: hevc_enc = "hevc_videotoolbox"
        if av1_enc or hevc_enc: return av1_enc, hevc_enc, "videotoolbox"
        
        # Fallback to Software if no HW found
        # libsvtav1 / libx265
        if "libsvtav1" in out: av1_enc = "libsvtav1"
        if "libx265" in out: hevc_enc = "libx265"
        
        if av1_enc or hevc_enc:
            return av1_enc, hevc_enc, "cpu"
        
        return None, None, "none"
        
    except Exception as e:
        print(f"[Warning] HW Detection failed: {e}")
        return None, None, "error"

def get_encoder_args(codec, encoder, qp):
    """Returns correct ffmpeg args for specific HW vendor"""
    if not encoder: return []
    
    # Software (CPU)
    if encoder == "libsvtav1":
        # CRF 0-63 (Lower is better)
        return ["-c:v", "libsvtav1", "-crf", str(qp), "-preset", "6", "-g", "240"]
        
    if encoder == "libx265":
        return ["-c:v", "libx265", "-crf", str(qp), "-preset", "medium"]

    # NVIDIA NVENC
    if "nvenc" in encoder:
        # p6 = better quality, spatial-aq for better perception
        return ["-c:v", encoder, "-rc", "constqp", "-qp", str(qp), "-preset", "p6", "-spatial-aq", "1"]
        
    # AMD AMF
    if "amf" in encoder:
        return ["-c:v", encoder, "-usage", "transcoding", "-rc", "cqp", "-qp_i", str(qp), "-qp_p", str(qp), "-qp_b", str(qp), "-quality", "quality"]

    # Intel QSV
    if "qsv" in encoder:
        return ["-c:v", encoder, "-global_quality", str(qp), "-look_ahead", "1"]
        
    # Apple VideoToolbox
    if "videotoolbox" in encoder:
        # Map 0-51 QP to 100-0 Quality (approx)
        q = int(100 - (qp * 2))
        return ["-c:v", encoder, "-q:v", str(q)]
        
    # Linux VAAPI
    if "vaapi" in encoder:
        # Uses -qp normally? or -global_quality? Depends on driver.
        # Often needs: -vf format=nv12,hwupload
        # Safe bet for vaapi is usually CQP via -qp or -global_quality
        return ["-c:v", encoder, "-qp", str(qp)]
        
    return []

def scan_directory(directory, extensions=None):
    """Scan directory for video files"""
    if extensions is None:
        extensions = {".mkv", ".mp4", ".mov", ".avi", ".ts"}

    files = []
    for dirpath, dirnames, filenames in os.walk(directory):
        # Skip processed/system files
        dirnames[:] = [d for d in dirnames if not d.startswith("_") and d not in [".recycle", "@eaDir"]]

        for filename in filenames:
            filepath = Path(dirpath) / filename
            if filepath.suffix.lower() in extensions:
                if "_av1" in filepath.stem.lower() or "_hevc" in filepath.stem.lower():
                    continue
                files.append(filepath)

    return sorted(files, key=lambda x: x.stat().st_size, reverse=True)
