import logging
import json
import os
import platform
import re
import shutil
import socket
import string
import subprocess
import sys
from datetime import datetime
from typing import Optional, List, Dict, Any

logger = logging.getLogger()


def launch_url(url: str) -> None:
    logger = logging.getLogger(__name__)

    if shutil.which('xdg-open'):
        opener = 'xdg-open'
    elif shutil.which('open'):
        opener = 'open'
    elif shutil.which('cmd'):
        opener = 'start'
    else:
        error_message = f"No valid launchers found to launch URL: {url}"
        logger.error(error_message)
        raise OSError(error_message)

    try:
        if opener == 'start':
            # For Windows, use 'cmd /c start'
            subprocess.run(['cmd', '/c', 'start', url], shell=False)
        else:
            subprocess.run([opener, url])
    except Exception as e:
        logger.error(f"Failed to launch URL: {url}. Error: {e}")


def file_exists_in_mounts(filepath: str) -> Optional[str]:
    """
    Check if a file exists in any mounted directory.
    It searches for the file in common mount points like '/Volumes', '/mnt', and '/media'.
    Returns the path to the file in the mount if found, otherwise returns None.

    Example:
    Before: filepath = '/path/to/file.txt'
    After: '/Volumes/ExternalDrive/path/to/file.txt'
    """

    def get_path_components(path):
        path = os.path.normpath(path)
        components = []
        while True:
            path, comp = os.path.split(path)
            if comp:
                components.append(comp)
            else:
                if path:
                    components.append(path)
                break
        components.reverse()
        return components

    # Get path components of the directory of the file
    path_components = get_path_components(os.path.dirname(filepath).strip('/'))

    # Iterate over possible root paths - this may need to be rethought for Windows support
    for root in ['/Volumes', '/mnt', '/media']:
        if os.path.exists(root):
            # Iterate over mounts in the root path
            for mount in os.listdir(root):
                # Since we don't know the home directory, we iterate until we find matching parts of the path
                matching_components = [s for s in path_components if s in mount]
                for component in matching_components:
                    possible_mount_path = os.path.join(root, mount, filepath.split(component)[-1].lstrip('/'))
                    if os.path.exists(possible_mount_path):
                        return possible_mount_path


def get_time_elapsed(start_time: Optional[datetime] = None, end_time: Optional[datetime] = None) -> str:

    def strfdelta(tdelta, fmt='%H:%M:%S'):
        days = tdelta.days
        hours, rem = divmod(tdelta.seconds, 3600)
        minutes, seconds = divmod(rem, 60)

        # Using f-strings for formatting
        formatted_str = fmt.replace('%D', f'{days}')
        formatted_str = formatted_str.replace('%H', f'{hours:02d}')
        formatted_str = formatted_str.replace('%M', f'{minutes:02d}')
        formatted_str = formatted_str.replace('%S', f'{seconds:02d}')
        return formatted_str

    # calculate elapsed time
    elapsed_time = None

    if start_time:
        if end_time:
            elapsed_time = end_time - start_time
        else:
            elapsed_time = datetime.now() - start_time

    elapsed_time_string = strfdelta(elapsed_time) if elapsed_time else None
    return elapsed_time_string


def get_file_size_human(file_path: str) -> str:
    size_in_bytes = os.path.getsize(file_path)

    # Convert size to a human-readable format
    if size_in_bytes < 1024:
        return f"{size_in_bytes} B"
    elif size_in_bytes < 1024 ** 2:
        return f"{size_in_bytes / 1024:.2f} KB"
    elif size_in_bytes < 1024 ** 3:
        return f"{size_in_bytes / 1024 ** 2:.2f} MB"
    elif size_in_bytes < 1024 ** 4:
        return f"{size_in_bytes / 1024 ** 3:.2f} GB"
    else:
        return f"{size_in_bytes / 1024 ** 4:.2f} TB"


def current_system_os() -> str:
    return platform.system().lower().replace('darwin', 'macos')


def current_system_os_version() -> str:
    return platform.release()


def current_system_cpu() -> str:
    return platform.machine().lower().replace('amd64', 'x64')


def current_system_cpu_brand() -> str:
    """Fast cross-platform CPU brand string"""
    if sys.platform.startswith('darwin'):  # macOS
        try:
            return subprocess.check_output(['sysctl', '-n', 'machdep.cpu.brand_string']).decode().strip()
        except Exception:
            pass
    elif sys.platform.startswith('win'):  # Windows
        from winreg import HKEY_LOCAL_MACHINE, OpenKey, QueryValueEx
        try:
            # Open the registry key where Windows stores the CPU name
            key = OpenKey(HKEY_LOCAL_MACHINE, r"HARDWARE\DESCRIPTION\System\CentralProcessor\0")
            # The value name is "ProcessorNameString"
            value, _ = QueryValueEx(key, "ProcessorNameString")
            return value.strip()  # Usually perfect, with full marketing name
        except Exception:
            # Fallback: sometimes the key is under a different index, try 1
            try:
                key = OpenKey(HKEY_LOCAL_MACHINE, r"HARDWARE\DESCRIPTION\System\CentralProcessor\1")
                value, _ = QueryValueEx(key, "ProcessorNameString")
                return value.strip()
            except Exception:
                return "Unknown CPU"
    elif sys.platform.startswith('linux'):
        try:
            with open('/proc/cpuinfo') as f:
                for line in f:
                    if line.startswith('model name'):
                        return line.split(':', 1)[1].strip()
        except Exception:
            pass

    # Ultimate fallback
    return platform.processor() or 'Unknown CPU'

def resources_dir() -> str:
    return os.path.join(os.path.dirname(__file__), '..', '..', 'resources')


def copy_directory_contents(src_dir: str, dst_dir: str) -> None:
    for item in os.listdir(src_dir):
        src_path = os.path.join(src_dir, item)
        dst_path = os.path.join(dst_dir, item)
        if os.path.isdir(src_path):
            shutil.copytree(src_path, dst_path)
        else:
            shutil.copy2(src_path, dst_path)


def check_for_updates(repo_name: str, repo_owner: str, app_name: str, current_version: str) -> Optional[Dict[str, Any]]:
    def get_github_releases(owner, repo):
        import requests
        url = f"https://api.github.com/repos/{owner}/{repo}/releases"
        try:
            response = requests.get(url, timeout=3)
            response.raise_for_status()
            releases = response.json()
            return releases
        except Exception as e:
            logger.error(f"Error checking for updates: {e}")
            return []

    releases = get_github_releases(repo_owner, repo_name)
    if not releases:
        return None

    latest_version = releases[0]
    latest_version_tag = latest_version['tag_name']

    from packaging import version
    if version.parse(latest_version_tag) > version.parse(current_version):
        logger.info(f"Newer version of {app_name} available. "
                    f"Latest: {latest_version_tag}, Current: {current_version}")
        return latest_version
    return None

def is_localhost(comparison_hostname: str) -> bool:
    return comparison_hostname in ['localhost', '127.0.0.1', socket.gethostname()]


def num_to_alphanumeric(num: int) -> str:
    return string.ascii_letters[num % 26] + str(num // 26)


def get_gpu_info() -> List[Dict[str, Any]]:
    """Cross-platform GPU information retrieval"""

    def get_windows_gpu_info():
        """Get GPU info on Windows"""
        try:
            result = subprocess.run(
                ['wmic', 'path', 'win32_videocontroller', 'get', 'name,AdapterRAM', '/format:list'], 
                capture_output=True, text=True, timeout=5
            )
            
            # Virtual adapters to exclude
            virtual_adapters = [
                'virtual', 'rdp', 'hyper-v', 'microsoft basic', 'basic display',
                'vga compatible', 'dummy', 'nvfbc', 'nvencode'
            ]
            
            gpus = []
            current_gpu = None
            
            for line in result.stdout.strip().split('\n'):
                line = line.strip()
                if not line:
                    continue
                    
                if line.startswith('Name='):
                    if current_gpu and current_gpu.get('name'):
                        gpus.append(current_gpu)
                    gpu_name = line.replace('Name=', '').strip()
                    
                    # Skip virtual adapters
                    if any(virtual in gpu_name.lower() for virtual in virtual_adapters):
                        current_gpu = None
                    else:
                        current_gpu = {'name': gpu_name, 'memory': 'Integrated'}
                    
                elif line.startswith('AdapterRAM=') and current_gpu:
                    vram_bytes_str = line.replace('AdapterRAM=', '').strip()
                    if vram_bytes_str and vram_bytes_str != '0':
                        try:
                            vram_gb = int(vram_bytes_str) / (1024**3)
                            current_gpu['memory'] = round(vram_gb, 2)
                        except:
                            pass
            
            if current_gpu and current_gpu.get('name'):
                gpus.append(current_gpu)
            
            return gpus if gpus else [{'name': 'Unknown GPU', 'memory': 'Unknown'}]
        except Exception as e:
            logger.error(f"Failed to get Windows GPU info: {e}")
            return [{'name': 'Unknown GPU', 'memory': 'Unknown'}]

    def get_macos_gpu_info():
        """Get GPU info on macOS (works with Apple Silicon)"""
        try:
            if current_system_cpu() == "arm64":
                # don't bother with system_profiler with Apple ARM - we know its integrated
                return [{'name': current_system_cpu_brand(), 'memory': 'Integrated'}]

            result = subprocess.run(['system_profiler', 'SPDisplaysDataType', '-detailLevel', 'mini', '-json'],
                                capture_output=True, text=True, timeout=5)
            data = json.loads(result.stdout)
            
            gpus = []
            displays = data.get('SPDisplaysDataType', [])
            for display in displays:
                if 'sppci_model' in display:
                    gpus.append({
                        'name': display.get('sppci_model', 'Unknown GPU'),
                        'memory': display.get('sppci_vram', 'Integrated'),
                    })
            return gpus if gpus else [{'name': 'Apple GPU', 'memory': 'Integrated'}]
        except Exception as e:
            print(f"Failed to get macOS GPU info: {e}")
            return [{'name': 'Unknown GPU', 'memory': 'Unknown'}]
        
    def get_linux_gpu_info():
        gpus = []
        try:
            # Run plain lspci and filter for GPU-related lines
            output = subprocess.check_output(
                ["lspci"], universal_newlines=True, stderr=subprocess.DEVNULL
            )
            for line in output.splitlines():
                if any(keyword in line.lower() for keyword in ["vga", "3d", "display"]):
                    # Extract the part after the colon (vendor + model)
                    if ":" in line:
                        name_part = line.split(":", 1)[1].strip()
                        # Clean up common extras like (rev xx) or (prog-if ...)
                        name = name_part.split("(")[0].split("controller:")[-1].strip()
                        vendor = "Unknown"
                        if "nvidia" in name.lower():
                            vendor = "NVIDIA"
                        elif "amd" in name.lower() or "ati" in name.lower():
                            vendor = "AMD"
                        elif "intel" in name.lower():
                            vendor = "Intel"
                        
                        gpus.append({
                            "name": name,
                            "vendor": vendor,
                            "memory": "Unknown"
                        })
        except FileNotFoundError:
            print("lspci not found. Install pciutils: sudo apt install pciutils")
            return []
        except Exception as e:
            print(f"Error running lspci: {e}")
            return []

        return gpus

    system = platform.system()
    
    if system == 'Darwin':  # macOS
        return get_macos_gpu_info()
    elif system == 'Windows':
        return get_windows_gpu_info()
    else:  # Assume Linux or other
        return get_linux_gpu_info()

COMMON_RESOLUTIONS = {
	# SD
	"SD_480p": (640, 480),
	"NTSC_DVD": (720, 480),
	"PAL_DVD": (720, 576),

	# HD
	"HD_720p": (1280, 720),
	"HD_900p": (1600, 900),
	"HD_1080p": (1920, 1080),

	# Cinema / Film
	"2K_DCI": (2048, 1080),
	"4K_DCI": (4096, 2160),

	# UHD / Consumer
	"UHD_4K": (3840, 2160),
	"UHD_5K": (5120, 2880),
	"UHD_8K": (7680, 4320),

	# Ultrawide / Aspect Variants
	"UW_1080p": (2560, 1080),
	"UW_1440p": (3440, 1440),
	"UW_5K": (5120, 2160),

	# Mobile / Social
	"VERTICAL_1080x1920": (1080, 1920),
	"SQUARE_1080": (1080, 1080),

	# Classic / Legacy
	"VGA": (640, 480),
	"SVGA": (800, 600),
	"XGA": (1024, 768),
	"WXGA": (1280, 800),
}

COMMON_FRAME_RATES = {
	"23.976 (NTSC Film)": 23.976,
	"24 (Cinema)": 24.0,
	"25 (PAL)": 25.0,
	"29.97 (NTSC)": 29.97,
	"30": 30.0,
	"48 (HFR Film)": 48.0,
	"50 (PAL HFR)": 50.0,
	"59.94": 59.94,
	"60": 60.0,
	"72": 72.0,
	"90 (VR)": 90.0,
	"120": 120.0,
	"144 (Gaming)": 144.0,
	"240 (HFR)": 240.0,
}