#!/usr/bin/python3


from __future__ import annotations

import json
import logging
import re
from pathlib import Path
from typing import Any, Union, cast

from slpkg.config import config_load
from slpkg.repositories import Repositories
from slpkg.utilities import Utilities
from slpkg.views.process import ViewProcess

# Initialize the logger for this module.
logger = logging.getLogger(__name__)


class InstallData:
    """Installs data to the repositories path."""

    def __init__(self) -> None:
        logger.debug("Initializing InstallData class.")
        self.cpu_arch = config_load.cpu_arch
        self.package_type = config_load.package_type

        self.utils = Utilities()
        self.repos = Repositories()
        self.view_process = ViewProcess()
        logger.debug("InstallData class initialized.")

    def write_repo_info(self, changelog_file: Path, info: dict[str, Union[str, bool]]) -> None:
        """Write some repo information.

        Args:
            changelog_file (Path): Repository ChangeLog.txt path.
            info (dict[str, Union[str, bool]]): Repository information.
        """
        repo_name: str = cast(str, info["repo_name"])
        full_requires: bool = cast(bool, info["full_requires"])
        last_date: str = ""
        repo_info: dict[str, Any] = {}

        logger.info("Writing repository info for '%s'. Changelog file: %s", repo_name, changelog_file)

        lines: list[str] = []
        if changelog_file.is_file():
            try:
                lines = self.utils.read_text_file(changelog_file)
                logger.debug("Read %d lines from changelog file: %s", len(lines), changelog_file)
            except Exception as e:  # pylint: disable=[W0718]
                logger.error("Failed to read changelog file '%s': %s", changelog_file, e)
        else:
            logger.warning("ChangeLog file not found at %s. Skipping date parsing.", changelog_file)

        days = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")
        for line in lines:
            if line.startswith(days):
                last_date = line.replace("\n", "")
                logger.debug("Found last updated date in changelog: '%s'", last_date)
                break
        else:
            logger.warning("No date found in changelog file '%s'. 'last_date' remains empty.", changelog_file)

        if self.repos.repos_information.is_file():
            try:
                repo_info = self.utils.read_json_file(self.repos.repos_information)
                logger.debug("Loaded existing repository information from: %s", self.repos.repos_information)
            except Exception as e:  # pylint: disable=[W0718]
                logger.error(
                    "Failed to read existing repository information from '%s': %s. Starting with empty info.",
                    self.repos.repos_information,
                    e,
                )
                repo_info = {}  # Reset to empty if reading fails.

        repo_info[repo_name] = {"last_updated": last_date, "full_requires": full_requires}
        logger.debug("Updated repo_info for '%s': %s", repo_name, repo_info[repo_name])

        try:
            self.repos.repos_information.write_text(json.dumps(repo_info, indent=4), encoding="utf-8")
            logger.info("Successfully wrote repository information to: %s", self.repos.repos_information)
        except OSError as e:
            logger.error("Failed to write repository information to '%s': %s", self.repos.repos_information, e)

    def install_sbo_data(self, repo: str) -> None:  # pylint: disable=[R0912,R0914,R0915]
        """Read the SLACKBUILDS.TXT FILE and creates a json data file.

        Args:
            repo (str): repository name.
        """
        logger.info("Installing SlackBuilds data for repository: %s", repo)
        self.view_process.message(f"Updating the database for {repo}")
        logger.debug("Displaying 'Updating the database' message for SBo repo '%s'.", repo)

        data: dict[str, dict[str, Union[str, list[str]]]] = {}
        cache: list[str] = []
        sbo_tags: list[str] = [
            "SLACKBUILD NAME:",
            "SLACKBUILD LOCATION:",
            "SLACKBUILD FILES:",
            "SLACKBUILD VERSION:",
            "SLACKBUILD DOWNLOAD:",
            "SLACKBUILD DOWNLOAD_x86_64:",
            "SLACKBUILD MD5SUM:",
            "SLACKBUILD MD5SUM_x86_64:",
            "SLACKBUILD REQUIRES:",
            "SLACKBUILD SHORT DESCRIPTION:",
        ]

        slackbuilds_txt_path: Path = Path(
            self.repos.repositories[repo]["path"], self.repos.repositories[repo]["slackbuilds_txt"]
        )
        slackbuilds_txt: list[str] = []
        try:
            slackbuilds_txt = slackbuilds_txt_path.read_text(encoding="utf-8").splitlines()
            logger.debug("Read %d lines from SLACKBUILDS.TXT: %s", len(slackbuilds_txt), slackbuilds_txt_path)
        except FileNotFoundError:
            logger.error("SLACKBUILDS.TXT file not found at: %s. Cannot install SBo data.", slackbuilds_txt_path)
            self.view_process.done()
            print()
            return
        except Exception as e:  # pylint: disable=[W0718]
            logger.error("Failed to read SLACKBUILDS.TXT from '%s': %s", slackbuilds_txt_path, e)
            self.view_process.done()
            print()
            return

        for i, line in enumerate(slackbuilds_txt, 1):  # pylint: disable=[R1702]
            for tag in sbo_tags:
                if line.startswith(tag):
                    line = line.replace(tag, "").strip()
                    cache.append(line)
                    break  # Break after finding a tag to avoid processing the same line multiple times.

            # Process a block of 10 tags (11 lines including the name line).
            if (i % 11) == 0 and len(cache) == 10:  # Ensure all 10 tags are captured.
                build: str = ""
                name: str = cache[0]
                version: str = cache[3]
                location: str = cache[1].split("/")[1]
                logger.debug("Processing SBo package: %s, Version: %s, Location: %s", name, version, location)

                requires_str = cache[8].replace("%README%", "").strip()
                requires_list = requires_str.split() if requires_str else []

                data[name] = {
                    "location": location,
                    "files": cache[2].split(),
                    "version": version,
                    "download": cache[4].split(),
                    "download64": cache[5].split(),
                    "md5sum": cache[6].split(),
                    "md5sum64": cache[7].split(),
                    "requires": requires_list,
                    "description": cache[9].replace(name, "").strip(),
                }

                arch: str = self.cpu_arch
                sbo_file: Path = Path(self.repos.repositories[repo]["path"], location, name, f"{name}.SlackBuild")
                if sbo_file.is_file():
                    try:
                        slackbuild_content = sbo_file.read_text(encoding="utf-8").splitlines()
                        for sbo_line in slackbuild_content:
                            if sbo_line.startswith("BUILD=$"):
                                build = "".join(re.findall(r"\d+", sbo_line))
                                logger.debug("Found BUILD variable for '%s': %s", name, build)
                            if sbo_line.startswith("ARCH=noarch"):
                                arch = "noarch"
                                logger.debug("Found ARCH=noarch for '%s'.", name)
                    except Exception as e:  # pylint: disable=[W0718]
                        logger.warning(
                            "Failed to read SlackBuild script '%s': %s. Using default build/arch.", sbo_file, e
                        )
                else:
                    logger.warning(
                        "SlackBuild script not found for '%s' at '%s'. Using default build/arch.", name, sbo_file
                    )

                # Construct package name based on detected arch and build
                package_full_name: str = (
                    f"{name}-{version}-{arch}-{build}{self.repos.repositories[repo]['repo_tag']}.tgz"
                )
                data[name].update({"arch": arch, "build": build, "package": package_full_name})
                logger.debug("Constructed full package name for '%s': %s", name, package_full_name)

                cache = []  # reset cache after processing a package.
            elif (i % 11) == 0 and len(cache) != 10:
                logger.warning(
                    "Skipping incomplete SBo package entry at line %d. Expected 10 tags, found %d. Cache: %s",
                    i,
                    len(cache),
                    ", ".join(cache),
                )
                cache = []  # Reset cache even if incomplete.

        repo_info: dict[str, Union[str, bool]] = {
            "repo_name": repo,
            "full_requires": False,  # SBo data typically doesn't have full_requires in the same way binary repos do.
        }

        path_changelog: Path = Path(
            self.repos.repositories[repo]["path"], self.repos.repositories[repo]["changelog_txt"]
        )
        self.write_repo_info(path_changelog, repo_info)
        logger.debug("Wrote repository info for SBo repo '%s'.", repo)

        data_file: Path = Path(self.repos.repositories[repo]["path"], self.repos.data_json)
        try:
            data_file.write_text(json.dumps(data, indent=4), encoding="utf-8")
            logger.info("Successfully wrote SBo data to JSON file: %s", data_file)
        except OSError as e:
            logger.error("Failed to write SBo data to '%s': %s", data_file, e)

        self.view_process.done()
        print()
        logger.info("SBo data installation for '%s' completed.", repo)

    def install_binary_data(self, repo: str) -> None:  # pylint: disable=[R0912,R0914,R0915]
        """Installs the data for binary repositories.

        Args:
            repo (str): Description
        """
        print()
        logger.info("Installing binary data for repository: %s", repo)
        self.view_process.message(f"Updating the database for {repo}")
        logger.debug("Displaying 'Updating the database' message for binary repo '%s'.", repo)

        slack_repos: list[str] = [self.repos.slack_patches_repo_name, self.repos.slack_extra_repo_name]

        mirror: str = self.repos.repositories[repo]["mirror_packages"]
        if repo in slack_repos:
            mirror = self.repos.repositories[repo]["mirror_changelog"]
            logger.debug("Adjusted mirror for Slack-like repo '%s' to changelog mirror: %s", repo, mirror)

        checksums_dict: dict[str, str] = {}
        data: dict[str, dict[str, Union[str, list[str]]]] = {}
        build: str = ""
        arch: str = ""
        requires: list[str] = []
        full_requires: bool = False
        pkg_tag = [
            "PACKAGE NAME:",
            "PACKAGE LOCATION:",
            "PACKAGE SIZE (compressed):",
            "PACKAGE SIZE (uncompressed):",
            "PACKAGE REQUIRED:",
            "PACKAGE DESCRIPTION:",
        ]
        path_packages: Path = Path(self.repos.repositories[repo]["path"], self.repos.repositories[repo]["packages_txt"])
        path_checksums: Path = Path(
            self.repos.repositories[repo]["path"], self.repos.repositories[repo]["checksums_md5"]
        )

        packages_txt: list[str] = []
        checksums_md5: list[str] = []

        try:
            packages_txt = self.utils.read_text_file(path_packages)
            logger.debug("Read %d lines from PACKAGES.TXT: %s", len(packages_txt), path_packages)
        except FileNotFoundError:
            logger.error("PACKAGES.TXT file not found at: %s. Cannot install binary data.", path_packages)
            self.view_process.done()
            print()
            return
        except Exception as e:  # pylint: disable=[W0718]
            logger.error("Failed to read PACKAGES.TXT from '%s': %s", path_packages, e)
            self.view_process.done()
            print()
            return

        try:
            checksums_md5 = self.utils.read_text_file(path_checksums)
            logger.debug("Read %d lines from CHECKSUMS.md5: %s", len(checksums_md5), path_checksums)
        except FileNotFoundError:
            logger.warning(
                "CHECKSUMS.md5 file not found at: %s. Checksums will be marked as 'error checksum'.", path_checksums
            )
        except Exception as e:  # pylint: disable=[W0718]
            logger.warning(
                "Failed to read CHECKSUMS.md5 from '%s': %s. Checksums might be affected.", path_checksums, e
            )

        for line in checksums_md5:
            line = line.strip()
            # Check if line ends with any of the defined package types (e.g., .tgz, .txz)
            if any(line.endswith(pkg_type) for pkg_type in self.package_type):
                parts = line.split("./")
                if len(parts) > 1:
                    file_part = parts[1].split("/")[-1].strip()
                    checksum_part = parts[0].strip()
                    checksums_dict[file_part] = checksum_part
                    logger.debug("Parsed checksum: file='%s', checksum='%s'", file_part, checksum_part)
                else:
                    logger.warning("Skipping malformed checksum line: '%s'", line)
            else:
                logger.debug("Skipping non-package checksum line: '%s'", line)

        cache: list[str] = []  # init cache

        for i, line in enumerate(packages_txt):
            if line.startswith(pkg_tag[0]):  # PACKAGE NAME:
                package = line.replace(pkg_tag[0], "").strip()
                parsed = self.utils.split_package(package)
                name = parsed["name"]
                version: str = parsed["version"]
                build = parsed["build"]
                arch = parsed["arch"]
                logger.debug(
                    "Processing package entry: %s (Name: %s, Version: %s, Build: %s, Arch: %s)",
                    package,
                    name,
                    version,
                    build,
                    arch,
                )

                cache.append(name)
                cache.append(version)
                cache.append(package)
                cache.append(mirror)
                try:
                    cache.append(checksums_dict[package])
                    logger.debug("Found checksum for '%s': %s", package, checksums_dict[package])
                except KeyError:
                    cache.append("error checksum")
                    logger.warning("Checksum not found for package '%s'. Marking as 'error checksum'.", package)

            elif line.startswith(pkg_tag[1]):  # PACKAGE LOCATION:
                package_location = line.replace(pkg_tag[1], "").strip()
                if repo == self.repos.slack_testing_repo_name:
                    cache.append(package_location.replace("./testing/", ""))
                else:
                    cache.append(package_location[2:])  # Do not install (.) dot
                logger.debug("Package location: %s", package_location[2:])

            elif line.startswith(pkg_tag[2]):  # PACKAGE SIZE (compressed):
                compressed_size = "".join(re.findall(r"\d+", line))
                cache.append(compressed_size)
                logger.debug("Compressed size: %s", compressed_size)

            elif line.startswith(pkg_tag[3]):  # PACKAGE SIZE (uncompressed):
                uncompressed_size = "".join(re.findall(r"\d+", line))
                cache.append(uncompressed_size)
                logger.debug("Uncompressed size: %s", uncompressed_size)

            elif line.startswith(pkg_tag[4]):  # PACKAGE REQUIRED:
                required = line.replace(pkg_tag[4], "").strip()
                requires = []  # Reset for each package.
                if "|" in required:
                    full_requires = True
                    deps_temp: list[str] = []
                    for req_part in required.split(","):
                        dep_options = req_part.split("|")
                        if len(dep_options) > 1:
                            deps_temp.append(dep_options[1].strip())  # Take the second option if '|' is present.
                        else:
                            deps_temp.extend([d.strip() for d in dep_options])  # Otherwise take all.
                    requires = list(set(deps_temp))  # Remove duplicates.
                    logger.debug("Full requires detected. Parsed dependencies: %s", ", ".join(requires))
                else:
                    requires = [r.strip() for r in required.split(",") if r.strip()]  # Split by comma and clean.
                    logger.debug("Simple requires detected. Parsed dependencies: %s", ", ".join(requires))

                # Ensure 'requires' is always a list, even if empty
                if not requires and required:
                    logger.warning("Could not parse requirements from string '%s'. Setting as empty list.", required)

            elif line.startswith(pkg_tag[5]):  # PACKAGE DESCRIPTION:
                # Description is on the next line, offset by (name length * 2) + 2
                package_description = ""
                if i + 1 < len(packages_txt):
                    # Ensure 'name' is available from cache[0] before using it for slicing
                    current_name_from_cache = cache[0] if len(cache) > 0 else ""
                    if current_name_from_cache:
                        desc_start_index = (len(current_name_from_cache) * 2) + 2
                        package_description = packages_txt[i + 1][desc_start_index:].strip()
                        logger.debug("Extracted description: '%s'", package_description)

                if not package_description:
                    package_description = "Not found"
                    logger.debug("Description not found for package. Setting to 'Not found'.")

                # Ensure description is wrapped in parentheses if it isn't already
                if not package_description.startswith("(") and not package_description.endswith(")"):
                    package_description = f"({package_description})"
                    logger.debug("Wrapped description in parentheses: '%s'", package_description)
                cache.append(package_description)

            # Check if all 9 expected items are in cache for a complete package entry.
            if len(cache) == 9:
                data[cache[0]] = {
                    "repo": repo,
                    "version": cache[1],
                    "package": cache[2],
                    "mirror": cache[3],
                    "checksum": cache[4],
                    "location": cache[5],
                    "size_comp": cache[6],
                    "size_uncomp": cache[7],
                    "description": cache[8],
                    "requires": requires,
                    "build": build,
                    "arch": arch,
                    "conflicts": "",  # Not parsed from PACKAGES.TXT.
                    "suggests": "",  # Not parsed from PACKAGES.TXT.
                }
                logger.debug("Added package '%s' data to dictionary. Data: %s", cache[0], data[cache[0]])

                cache = []  # reset cache for the next package.
                requires = []  # reset requires for the next package.
                build = ""  # Reset build and arch for the next package.
                arch = ""

        repo_info: dict[str, Union[str, bool]] = {
            "repo_name": repo,
            "full_requires": full_requires,  # This indicates if any package in this repo had '|' dependencies.
        }

        path_changelog: Path = Path(
            self.repos.repositories[repo]["path"], self.repos.repositories[repo]["changelog_txt"]
        )
        self.write_repo_info(path_changelog, repo_info)
        logger.debug("Wrote repository info for binary repo '%s'.", repo)

        data_file: Path = Path(self.repos.repositories[repo]["path"], self.repos.data_json)
        try:
            data_file.write_text(json.dumps(data, indent=4), encoding="utf-8")
            logger.info("Successfully wrote binary data to JSON file: %s", data_file)
        except OSError as e:
            logger.error("Failed to write binary data to '%s': %s", data_file, e)

        self.view_process.done()
        print()
        logger.info("Binary data installation for '%s' completed.", repo)
