"""Core logic for Manjaro Log Helper.

Backend functionality: collecting system logs, removing personal data,
and sending to dpaste.com. No GUI dependencies.
Privacy-first: aggressive regex scrubbing for online sharing.
"""

import os
import re
import shutil
import subprocess
import gettext
import socket
import urllib.parse
from pathlib import Path
from datetime import datetime

# Set up gettext for translations
import os
base_dir = os.path.abspath(os.path.dirname(__file__))
local_locale_dir = os.path.join(base_dir, "locale")
system_locale_dir = os.path.join(base_dir, "usr", "share", "locale")

if os.path.exists(local_locale_dir):
    locale_dir = local_locale_dir
elif os.path.exists(system_locale_dir):
    locale_dir = system_locale_dir
else:
    locale_dir = "/usr/share/locale"

gettext.bindtextdomain("manjaro-log-helper", locale_dir)
gettext.textdomain("manjaro-log-helper")
_ = gettext.gettext

# Version
__version__ = "1.0.2"

def get_version() -> str:
    """Return the current version of Manjaro Log Helper."""
    return __version__

DEFAULT_EXPIRE_DAYS = 90

_SCRUB_MARKER = "### manjaro-log-helper: personal data removed ###"

DIR_OF_CONFIG = Path.home() / ".config" / "manjaro-log-helper"
DIR_OF_CACHE = Path.home() / ".cache" / "manjaro-log-helper"
FILE_OF_LOGS = DIR_OF_CACHE / "manjaro-log-helper.logs"
URL_LIST = DIR_OF_CONFIG / "url-list.txt"

LOG_COMMANDS = {
    "check_journal0": ("pkexec", ["journalctl", "-b", "0", "--no-pager"]),
    "check_journal1": ("pkexec", ["journalctl", "-b", "-1", "--no-pager"]),
    "check_journal2": ("pkexec", ["journalctl", "-b", "-2", "--no-pager"]),
    "check_install": ("cat", ["/var/log/Calamares.log"]),
    "check_blame": (None, ["systemd-analyze", "blame"]),
    "check_lspci": (None, ["lspci", "-vnn"]),
    "check_lspcit": (None, ["lspci", "-vt"]),
    "check_lsusb": (None, ["lsusb", "-vt"]),
    "check_inxi": (None, ["inxi", "-Fxzc0"]),
}


def header(title: str) -> str:
    return f"\n#################### {title} ########################\n"


def footer() -> str:
    return "\n\n"


def _run_command(prefix, cmd):
    if prefix == "pkexec":
        full = ["pkexec"] + list(cmd) if shutil.which("pkexec") else list(cmd)
    elif prefix == "cat":
        full = ["cat", cmd[0]]
    else:
        full = list(cmd)

    try:
        result = subprocess.run(full, capture_output=True, text=True, check=False)
    except FileNotFoundError as exc:
        missing = getattr(exc, "filename", full[0])
        return f"Sorry, command '{missing}' not found.\n"

    return result.stdout + result.stderr


def collect_logs(selected_ids):
    DIR_OF_CONFIG.mkdir(parents=True, exist_ok=True)
    DIR_OF_CACHE.mkdir(parents=True, exist_ok=True)
    FILE_OF_LOGS.unlink(missing_ok=True)

    with FILE_OF_LOGS.open("w", encoding="utf-8") as logfile:
        logfile.write(f"Generated on {_now()}\n\n")
        for cid in selected_ids:
            if cid not in LOG_COMMANDS:
                continue
            prefix, cmd = LOG_COMMANDS[cid]
            title = " ".join(cmd)
            logfile.write(header(title))
            logfile.write(_run_command(prefix, cmd))
            logfile.write(footer())


def _now() -> str:
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")


def user_names(low: int = 1000, high: int = 50000):
    try:
        with open("/etc/passwd", encoding="utf-8") as f:
            for line in f:
                parts = line.split(":")
                if len(parts) < 3:
                    continue
                name, _, uid = parts[0], parts[1], parts[2]
                try:
                    if low <= int(uid) <= high:
                        yield name
                except ValueError:
                    continue
    except FileNotFoundError:
        return


def remove_personal_data(force: bool = False):
    """Aggressively sanitize logs for safe online sharing."""
    if not FILE_OF_LOGS.is_file():
        raise FileNotFoundError("No logs!")

    text = FILE_OF_LOGS.read_text(encoding="utf-8", errors="replace")

    if not force and _SCRUB_MARKER in text:
        return

    DIR_OF_CACHE.mkdir(parents=True, exist_ok=True)
    shutil.copy2(FILE_OF_LOGS, FILE_OF_LOGS.with_suffix(".logs.bak"))

    hostname = os.uname().nodename

    for user in user_names():
        text = _scrub_token(text, user, "_user_")
    text = _scrub_token(text, hostname, "_hostname_")
    
    # Fallback: Blind scrubbing of /home/username/ paths
    text = re.sub(r"/home/[^/\s]+", "/home/_user_", text)

    text = _scrub_generic(text)
    text = _scrub_secrets(text)

    text = f"{_SCRUB_MARKER}\n{text}"
    FILE_OF_LOGS.write_text(text, encoding="utf-8")

    filter_script = DIR_OF_CONFIG / "manjaro-help-filter.sh"
    if filter_script.is_file():
        subprocess.run(["bash", str(filter_script), str(FILE_OF_LOGS)], check=False)


def _scrub_token(text: str, token: str, replacement: str) -> str:
    if not token:
        return text
    pattern = re.compile(rf"\b{re.escape(token)}\b", re.IGNORECASE)
    return pattern.sub(replacement, text)


_GENERIC_SCRUB_PATTERNS = [
    (re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b"), "_ip_"),
    (re.compile(r"(?i)(?:[a-f0-9]{1,4}:){2,7}[a-f0-9]{1,4}|::(?:[a-f0-9]{1,4}:){0,5}[a-f0-9]{1,4}|[a-f0-9]{1,4}::(?:[a-f0-9]{1,4}:){0,5}[a-f0-9]{1,4}"), "_ipv6_"),
    (re.compile(r"\b(?:[0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}\b|\b(?:[0-9A-Fa-f]{4}\.){2}[0-9A-Fa-f]{4}\b"), "_mac_"),
    (re.compile(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b"), "_email_"),
    (re.compile(r"\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b"), "_uuid_"),
]


def _scrub_generic(text: str) -> str:
    for pattern, replacement in _GENERIC_SCRUB_PATTERNS:
        text = pattern.sub(replacement, text)
    return text


def _scrub_secrets(text: str) -> str:
    """Remove accidental secrets (passwords, tokens) that might appear in logs."""
    secrets_pattern = re.compile(
        r'(?:password|passwd|pwd|token|api[_-]?key|secret|auth)(?:\s*[:=]\s*)["\']?([^"\'\s\n]+)["\']?',
        re.IGNORECASE
    )
    return secrets_pattern.sub(lambda m: m.group(0).replace(m.group(1), "_REDACTED_"), text)


def send_to_internet(expire_days: int = DEFAULT_EXPIRE_DAYS) -> str:
    remove_personal_data()
    url = _send_with_http_client(expire_days)

    DIR_OF_CONFIG.mkdir(parents=True, exist_ok=True)
    with URL_LIST.open("a", encoding="utf-8") as f:
        f.write(f"{_now()}: {url}\n")

    return url


def _send_with_http_client(expire_days: int) -> str:
    """Upload the log file to dpaste.com, automatically following HTTP redirects (301/302).

    Falls back to www.dpaste.com if dpaste.com does not resolve (DNS issue).
    """
    import http.client

    boundary = "----manjaro_log_helper_boundary"
    log_data = FILE_OF_LOGS.read_bytes()

    crlf = b"\r\n"
    body = b""
    body += f"--{boundary}".encode() + crlf
    body += b'Content-Disposition: form-data; name="expiry_days"' + crlf + crlf
    body += str(expire_days).encode() + crlf
    body += f"--{boundary}".encode() + crlf
    body += b'Content-Disposition: form-data; name="content"' + crlf
    body += b"Content-Type: text/plain" + crlf + crlf
    body += log_data + crlf
    body += f"--{boundary}--".encode() + crlf

    headers = {
        "Content-Type": f"multipart/form-data; boundary={boundary}",
        "User-Agent": "manjaro-log-helper",
    }

    def _make_request(host: str, path: str, redirect_count: int = 0):
            if redirect_count > 3:
                raise RuntimeError(_("Too many redirects (redirect loop)."))

            conn = http.client.HTTPSConnection(host, timeout=30)
            try:
                conn.request("POST", path, body=body, headers=headers)
                resp = conn.getresponse()

                # Automatically handle redirects (301, 302, 303, 307, 308)
                if resp.status in (301, 302, 303, 307, 308):
                    location = resp.getheader("Location")
                    resp.read()  # Consume response body to avoid protocol errors
                    conn.close()
                    if location:
                        parsed = urllib.parse.urlparse(location)
                        new_host = parsed.netloc or host
                        new_path = parsed.path or path
                        return _make_request(new_host, new_path, redirect_count + 1)

                payload = resp.read().decode("utf-8")
                if resp.status not in (200, 201):
                    raise RuntimeError(f"dpaste.com returned HTTP {resp.status}: {payload}")
                return payload.strip()

            except http.client.HTTPException as exc:
                raise RuntimeError(_("Connection error to dpaste.com: {}").format(exc)) from exc
            finally:
                conn.close()

    # Try dpaste.com first, fall back to www.dpaste.com on DNS failure.
    hosts_to_try = ["dpaste.com", "www.dpaste.com"]

    for host in hosts_to_try:
        try:
            return _make_request(host, "/api/v2/")
        except socket.gaierror as exc:
            if host != hosts_to_try[-1]:
                continue  # Try next host
            raise RuntimeError(
                _("Unable to resolve server name. "
                  "Check your internet connection.")
            ) from exc
        except RuntimeError:
            raise  # Other errors (HTTP, redirect loop) do not trigger fallback

def copy_to_clipboard(text: str) -> bool:
    try:
        result = subprocess.run(["wl-copy"], input=text, text=True, check=False)
        if result.returncode == 0:
            return True
    except FileNotFoundError:
        pass

    try:
        result = subprocess.run(
            ["xclip", "-i", "-selection", "clipboard"],
            input=text, text=True, check=False
        )
        return result.returncode == 0
    except FileNotFoundError:
        return False


def open_url(url: str) -> bool:
    try:
        result = subprocess.run(["xdg-open", url], check=False)
    except FileNotFoundError:
        return False
    return result.returncode == 0


def open_log_file() -> bool:
    try:
        result = subprocess.run(["xdg-open", str(FILE_OF_LOGS)], check=False)
    except FileNotFoundError:
        return False
    return result.returncode == 0