import os
import platform
import ctypes
import psutil
import subprocess
import socket
import sys
import tempfile
import time
import asyncio
from pathlib import Path
from typing import Optional

from ws_package.log.writer import LogWriter

logger = LogWriter.file(__name__)


def get_os_type():
    """Get the type of operating system."""
    return platform.system()


class IdleDuration:
    name: str = get_os_type()

    def value(self):
        if self.name == "Windows":
            return self.get_idle_duration_windows()
        elif self.name == "Linux":
            return self.get_idle_duration_linux()
        elif self.name == "Darwin":
            return self.get_idle_duration_mac()
        else:
            raise NotImplementedError(f"OS {self.name} not supported")
    
    def get_idle_duration_windows(self):
        class LASTINPUTINFO(ctypes.Structure):
            _fields_ = [("cbSize", ctypes.c_uint), ("dwTime", ctypes.c_int)]

        lii = LASTINPUTINFO()
        lii.cbSize = ctypes.sizeof(LASTINPUTINFO)
        ctypes.windll.user32.GetLastInputInfo(ctypes.byref(lii))
        millis = ctypes.windll.kernel32.GetTickCount() - lii.dwTime
        return millis / 1000.0
    
    def get_idle_duration_linux(self):
        try:
            idle_time = self.get_idle_time()
            idle_time_seconds = idle_time / 1000  # Convert mili seconds to second
            return idle_time_seconds
        except Exception as e:
            logger.error("Error in get_idle_duration_linux: %s", e)
            return 0
    
    def get_idle_time(self):
        try:
            idle_time = psutil.cpu_times().idle
            return idle_time
        except Exception as e:
            logger.error("Error in get_idle_time: %s", e)
            return 0
    
    def get_idle_duration_mac(self):
        try:
            idle_time = subprocess.check_output(['ioreg', '-c', 'IOHIDSystem']).decode('ascii')
            idle_time = [line for line in idle_time.splitlines() if 'HIDIdleTime' in line][0]
            idle_time = int(idle_time.split('=')[-1].strip()[:-1])
            return idle_time / 1000000000.0  # Convert nanoseconds to seconds
        except Exception as e:
            logger.error("Error in get_idle_duration_mac: %s", e)
            return 0


def get_ip():
    """Best-effort local IP. Must not block after sleep/wake (no default TCP hang)."""
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        try:
            s.settimeout(2.0)
            s.connect(("8.8.8.8", 80))
            return s.getsockname()[0]
        finally:
            s.close()
    except (socket.error, OSError, TimeoutError):
        return None


def check_internet():
    try:
        # Try to resolve a well-known host (like Google's public DNS server)
        socket.create_connection(("8.8.8.8", 53), timeout=5)
        return True
    except OSError:
        return False


def get_os_version():
    """Get the type of operating system."""
    return platform.platform()


def resolve_screenshot_directory(configured: Optional[str] = None) -> str:
    """
    Return a writable absolute directory for screenshot JPEG storage.

    PyInstaller onedir builds often run with cwd under _internal/, which is not
    writable. Frozen Windows agents therefore always use %APPDATA%\\Workstatus\\screenshots.
    """
    folder_name = (configured or "screenshots").strip() or "screenshots"

    if sys.platform == "darwin":
        base = Path.home() / "Library/Application Support/Workstatus" / folder_name
    elif sys.platform == "win32":
        if getattr(sys, "frozen", False):
            base = Path(os.environ.get("APPDATA", str(Path.home()))) / "Workstatus" / folder_name
        else:
            p = Path(folder_name)
            base = p if p.is_absolute() else Path.cwd() / p
    else:
        p = Path(folder_name)
        base = p if p.is_absolute() else Path.home() / "Workstatus" / folder_name

    try:
        base.mkdir(parents=True, exist_ok=True)
        return str(base)
    except (PermissionError, OSError) as e:
        fallback = Path(tempfile.gettempdir()) / "Workstatus" / folder_name
        fallback.mkdir(parents=True, exist_ok=True)
        logger.warning(
            "Screenshot directory %s not writable (%s); using %s",
            base, e, fallback,
        )
        return str(fallback)


def run(cmd):
    try:
        return subprocess.run(cmd, shell=True, capture_output=True, check=True, encoding="utf-8").stdout.strip()
    except Exception:
        return None


async def arun(cmd):
    try:
        proc = await asyncio.create_subprocess_shell(
            cmd,
            stdout=asyncio.subprocess.PIPE,
            stdin=asyncio.subprocess.PIPE
        )
        stdout, stderr = await proc.communicate()
        return stdout.decode()
    except Exception as e:
        return None


def guid():
    if sys.platform == 'darwin':
        return run("ioreg -d2 -c IOPlatformExpertDevice | awk -F\\\" '/IOPlatformUUID/{print $(NF-1)}'")
    
    if sys.platform == 'win32' or sys.platform == 'cygwin' or sys.platform == 'msys':
        return run('wmic csproduct get uuid').split('\n')[2].strip()

    if sys.platform.startswith('linux'):
        return run('cat /var/lib/dbus/machine-id') or run('cat /etc/machine-id')

    if sys.platform.startswith('openbsd') or sys.platform.startswith('freebsd'):
        return run('cat /etc/hostid') or run('kenv -q smbios.system.uuid')


async def aguid():
    if sys.platform == 'darwin':
        return await arun("ioreg -d2 -c IOPlatformExpertDevice | awk -F\\\" '/IOPlatformUUID/{print $(NF-1)}'")
    
    if sys.platform == 'win32' or sys.platform == 'cygwin' or sys.platform == 'msys':
        return await arun('wmic csproduct get uuid').split('\n')[1].strip()

    if sys.platform.startswith('linux'):
        return await arun('cat /var/lib/dbus/machine-id') or await arun('cat /etc/machine-id')

    if sys.platform.startswith('openbsd') or sys.platform.startswith('freebsd'):
        return await arun('cat /etc/hostid') or await arun('kenv -q smbios.system.uuid')


async def precise_sleep(target_duration, stop_event=None):
    """
    Sleeps for the target duration. This function minimizes drift by combining
    asyncio.sleep and a busy-wait loop, and uses time.perf_counter for high-resolution timing.

    Args:
        target_duration (float): Duration in seconds to sleep.
        stop_event (asyncio.Event, optional): An event to signal early termination.
    """
    start_time = time.perf_counter()

    # Sleep for most of the duration
    while True:
        if stop_event and stop_event.is_set():
            break

        elapsed_time = time.perf_counter() - start_time
        remaining_time = target_duration - elapsed_time

        if remaining_time <= 0:
            break

        # Use asyncio.sleep for remaining time larger than a threshold
        if remaining_time > 0.005:  # Sleep for 5 milliseconds or more
            # Sleep in smaller chunks to allow checking stop_event
            sleep_chunk = min(remaining_time / 2, 0.1)  # Sleep for a max of 100ms at a time
            await asyncio.sleep(sleep_chunk)
        else:
            # Busy-wait loop for final precision
            while time.perf_counter() - start_time < target_duration:
                if stop_event and stop_event.is_set():
                    break
                pass