#!/usr/bin/python3
# -*- coding: utf-8 -*-

import configparser
import getpass
import logging
import subprocess
from pathlib import Path
from typing import Optional

import urllib3
from urllib3.exceptions import HTTPError

from sun.__metadata__ import __prgnam__
from sun.configs import DAEMON_PID_FILE, configs_load
from sun.sys_info import get_os_info

logger = logging.getLogger(__name__)


class Utilities:
    """General utility methods for file I/O, process management and system info display.
    """

    def __init__(self) -> None:
        self.configs = configs_load
        self.changelog_url: str = f'{self.configs.repo_mirror}{self.configs.repo_log_file}'

    def read_repo_text_file(self) -> str:
        """Fetches the remote repository ChangeLog.txt file over HTTP.

        Returns:
            str: The ChangeLog.txt file content, or empty string on error.
        """
        log_txt: str = ''
        try:
            http = urllib3.PoolManager()
            con = http.request('GET', self.changelog_url, timeout=self.configs.http_timeout)
            log_txt = con.data.decode('utf-8', errors='replace')
        except urllib3.exceptions.LocationValueError:
            print('SUN: Error: Ftp mirror not supported')
            logger.error('Ftp mirror not supported: %s', self.changelog_url)
        except HTTPError:
            print(f'SUN: Error: Failed to connect to {self.changelog_url}')
            logger.error('Failed to connect to %s', self.changelog_url)

        return log_txt

    def fetch_if_modified(self, last_modified: Optional[str] = None) -> tuple[Optional[str], Optional[str]]:
        """Fetches the remote ChangeLog only if it has changed since last_modified.

        Sends an If-Modified-Since header when last_modified is provided.
        Returns (None, last_modified) on 304 Not Modified or on network error,
        so the caller can keep using its cached copy.

        Args:
            last_modified (Optional[str]): Value of a previous Last-Modified header.

        Returns:
            tuple[Optional[str], Optional[str]]: (content, new_last_modified).
                content is None if the file is unchanged or could not be fetched.
        """
        try:
            http = urllib3.PoolManager()
            headers = {'If-Modified-Since': last_modified} if last_modified else {}
            con = http.request('GET', self.changelog_url, headers=headers, timeout=self.configs.http_timeout)
            if con.status == 304:
                return None, last_modified
            new_lm: Optional[str] = con.headers.get('Last-Modified')
            return con.data.decode('utf-8', errors='replace'), new_lm
        except urllib3.exceptions.LocationValueError:
            logger.error('Ftp mirror not supported: %s', self.changelog_url)
        except HTTPError:
            logger.error('Failed to connect to %s', self.changelog_url)
        return None, last_modified

    @staticmethod
    def read_local_text_file(registry: Path) -> str:
        """Reads a local text file and returns its content.

        Args:
            registry (Path): Path to the local file to read.

        Returns:
            str: The file content, or empty string if the file does not exist.
        """
        log_txt: str = ''
        if registry.is_file():
            with open(registry, 'r', encoding='utf-8', errors='ignore') as file_txt:
                log_txt = file_txt.read()
        else:
            print(f"\nSUN: Error: Failed to find '{registry}' file.\n")
            logger.error("Failed to find '%s' file", registry)
        return log_txt

    @staticmethod
    def convert_sizes(byte_size: float) -> str:
        """Converts a byte value to a human-readable size string.

        Args:
            byte_size (float): The size in bytes.

        Returns:
            str: Human-readable size string (e.g. '5 MB', '2 GB').
        """
        kb_size: float = byte_size / 1024
        mb_size: float = kb_size / 1024
        gb_size: float = mb_size / 1024

        if gb_size >= 1:
            return f"{gb_size:.0f} GB"
        if mb_size >= 1:
            return f"{mb_size:.0f} MB"
        if kb_size >= 1:
            return f"{kb_size:.0f} KB"

        return f"{byte_size} B"

    def get_daemon_pid(self) -> str:
        """Read and return the current PID of sun-daemon
        from /home/user/.run/sun-daemon.pid file.

        Returns:
            str: PID of sun-daemon.
        """
        if DAEMON_PID_FILE.is_file():
            pid = Utilities.read_local_text_file(DAEMON_PID_FILE)
            return pid.strip()
        return ''

    @staticmethod
    def process_pids(process_name: str) -> list[int]:
        """Finds the PIDs of an application using the `ps aux` command.

        Args:
            process_name (str): The name of the process.

        Returns:
            list[int]: List of process IDs.
        """
        processes: list[int] = []
        try:
            output = subprocess.check_output(['ps', 'aux']).decode("utf-8")

            for line in output.splitlines():
                if process_name in line:
                    try:
                        pid = int(line.split()[1])
                        processes.append(pid)
                    except (IndexError, ValueError):
                        logger.error('Error parsing `ps aux` output line: %s', line)
        except subprocess.CalledProcessError:
            print("SUN: Error executing `ps aux` command.")
            logger.error('Error executing `ps aux` command')
        return processes

    def mirror_version_warning(self) -> str:
        """Returns a warning string if the configured mirror does not match
        the installed Slackware version, or empty string if they match.

        Reads /etc/slackware-version to detect stable (e.g. '15.0') vs
        current ('15.0+'), then checks whether the mirror URL contains the
        expected path segment ('slackware64-15.0' or 'slackware64-current').

        Returns:
            str: Warning message, or empty string if mirror matches.
        """
        mirror = self.configs.repo_mirror
        try:
            version_line = Path('/etc/slackware-version').read_text(encoding='utf-8').strip()
            is_current = version_line.endswith('+')
            expected = 'current' if is_current else version_line.split()[-1].rstrip('+')
            if expected not in mirror:
                installed = 'current' if is_current else expected
                return (
                    f'Warning: mirror may not match installed Slackware version '
                    f'({installed}). Update HTTP_MIRROR in sun.conf.'
                )
        except OSError:
            pass
        return ''

    def config_html(self) -> str:
        """Returns HTML-formatted sun.conf configuration contents.

        Returns:
            str: HTML string with section headers and aligned key = value pairs.
        """
        sun_conf = Path(self.configs.data_configs['sun_conf_path'], f'{__prgnam__}.conf')
        config = configparser.ConfigParser()
        config.read(sun_conf)
        if not config.sections():
            return ''
        s: str = '<span style="color: #00bcd4;"><b>'
        e: str = '</b></span>'
        width = max(len(k) for sec in config.sections() for k in config[sec])
        lines: list[str] = []
        for section in config.sections():
            lines.append(f'{s}[{section}]{e}')
            for key, val in config[section].items():
                lines.append(f'<b>{key.upper():<{width}}</b>  =  {val}')
            lines.append('')
        body = '\n'.join(lines).strip()
        return f'<pre style="font-family: monospace;">{body}</pre>'

    def os_info_html(self) -> str:
        """Returns HTML-formatted system, CPU, memory, disk and GPU information.

        Returns:
            str: HTML string with system information sections.
        """
        dc = get_os_info()
        dc['pkg_path'] = self.configs.data_configs['pkg_path']
        s: str = '<span style="color: #00bcd4;"><b>'
        e: str = '</b></span>'
        html_text: str = f"""{s}[System]{e}<br>
<b>User</b>: {getpass.getuser()}<br>
<b>OS</b>: {dc['os_name']}<br>
<b>Desktop</b>: {dc['desktop']}<br>
<b>Hostname</b>: {dc['hostname']}<br>
<b>Arch</b>: {dc['arch']}<br>
<b>Packages</b>: {len(list(dc['pkg_path'].iterdir()))}<br>
<b>Kernel</b>: {dc['kernel']}<br>
<b>Uptime</b>: {dc['uptime']}<br><br>
{s}[Processor]{e}<br>
<b>CPU</b>: {dc['cpu']}<br>
<b>Cores</b>: {dc['cpu_cores']}<br>
<b>Logical Cores</b>: {dc['logical_cores']}<br><br>
{s}[Memory]{e}<br>
<b>Free</b>: {Utilities.convert_sizes(dc['mem_free'])}<br>
<b>Used</b>: {Utilities.convert_sizes(dc['mem_used'])}<br>
<b>Total</b>: {Utilities.convert_sizes(dc['mem_total'])}<br>
<b>Percent</b>: {dc['mem_percent']}%<br><br>
{s}[Disk]{e}<br>
<b>Free</b>: {Utilities.convert_sizes(dc['disk_free'])}<br>
<b>Used</b>: {Utilities.convert_sizes(dc['disk_used'])}<br>
<b>Total</b>: {Utilities.convert_sizes(dc['disk_total'])}<br>
<b>Percent</b>: {dc['disk_percent']}%<br>
<b>Type</b>: {dc['disk_type']}<br><br>
{s}[GPU]{e}<br>
<b>VGA</b>: {dc['gpu']}"""
        return html_text
