"""Generate different block list formats from source files"""

import argparse
import difflib
import functools
import operator
import re
from enum import Enum
from pathlib import Path

from helper import compare_url_subsets, write_list_from_lines


class ListFormat(Enum):
    """Class enumerating block list formats"""

    UBLACKLIST = "ublacklist"
    ADBLOCK = "adblock"
    DNSMASQ = "dnsmasq"
    HOSTSETC = "hostsetc"
    HOSTSIP4 = "hostsip4"
    HOSTSIP6 = "hostsip6"


class ListSuffix(Enum):
    """Class enumerating source list suffixes"""

    ALL = ".all"
    INDIEWIKI = "-by-indie-wiki"
    WIKIGG = "-by-wiki-gg"
    GAMING = "-gaming"
    TECH = "-tech"
    NONE = ""


ROOT_PATH = Path(__file__).parent.parent
FORMAT_PATH = Path(ROOT_PATH, "by-format")
SOURCE_PATH = Path(ROOT_PATH, "sources")
REPO_URL = "https://github.com/codeshell/blocklists/"
FORMAT_URL = REPO_URL + "tree/main/by-format/"


def get_source_file_lines(filename) -> list[str] | None:
    """Try to read file as list of strings"""
    try:
        with open(filename, "rt", encoding="utf-8") as fp:
            return fp.readlines()
    except FileNotFoundError:
        print(f"Please check existence of file {filename}.")
        return None
    except OSError as e:
        print(f"File {filename} not loaded: {e}.")
        return None


def sanitize_lines(lines: list[str]) -> list[str]:
    """
    lines from files will most likely contain whitespaces such as newlines.
    They must be removed first because they will likely break the following string operations.
    Additionally (not! overwriting the default), this strips any leading and trailing slashes.
    """
    return [x.strip().strip(r"\/ ") for x in lines]


def optimize_lines(lines: list[str]) -> list[str]:
    """
    NOT IMPLEMENTED YET
    Remove lines that are only a subset of another line
    """
    # NOTE: check, if one entry is part of another rule
    # This hits pretty hard performance-wise on big lists
    # because the check must iterate lines as n:n

    optimized_lines = [
        search_str
        for search_str in lines
        if not any(compare_url_subsets(main_str, search_str) for main_str in lines if search_str != main_str)
    ]

    # removed = difflib.unified_diff(lines, result, "full", "stripped", n=0, lineterm="")
    # print("\n".join(filter(lambda x: x[1] != "@", removed)))

    i_old = len(lines)
    i_new = len(optimized_lines)
    if i_new < i_old:
        print(f"Optimization returned {i_new} from {i_old} entries. ({i_new - i_old})")

    # NOTE: no need to sort and remove duplicates here, because this
    # is done only and directly before writing files to storage
    # with helper.write_list_from_lines(). optimize_lines() would
    # catch most but not all cases (e.g. generate_format() changes the
    # lines output for multi-variant lists) and might run multiple times.

    # optimized_lines = sorted(set(optimized_lines))

    return optimized_lines


class UnwantedSites:
    """
    Present lines in different flavors as supported by lists.

    This should not be done inline/on-the-fly because lines need to be optimized
    after changing to prevent unnecessary duplicates
    """

    def __init__(self, lines: list[str], label: str):
        """
        lines: as given by source

        label: identifies the source data context
        """
        self._lines_up_to_domain = None
        self._lines_up_to_path = None
        self.label = label
        self.lines = lines
        self.list_description = ""
        self.list_variants: list[str] = []

    @property
    def lines(self) -> list[str]:
        """lines as presented by source"""
        return self._lines

    @lines.setter
    def lines(self, value: list[str]):
        self._lines = value
        self._update_line_flavors()

    @property
    def lines_up_to_subdomain(self) -> list[str]:
        """lines containing domain + subdomain"""
        return self._lines_up_to_domain

    @property
    def lines_up_to_path(self) -> list[str]:
        """lines containing domain + subdomain + path"""
        return self._lines_up_to_path

    @property
    def list_name(self) -> list[str]:
        """Public name of a list"""
        return f"TMW BL {self.label}"

    def _notice_if_lines_adjusted(self, a, b):
        """
        If the source values are adjusted because of input validation,
        output the difference because they indicate potential errors in the source data
        """
        test_lines = difflib.unified_diff(a, b, "Source", "Adjusted", n=0, lineterm="")
        test_result = "\n".join(test_lines)
        if test_result:
            print(f"There where changes made when ingesting source data ({self.label}).")
            print("Please check if they are intended:")
            print(test_result)
        else:
            print(f"No crazy stuff found ({self.label}).")

    def _update_line_flavors(self):
        """when lines are changed, (re-)generate all line flavors"""
        lines = self._lines

        path_pattern = re.compile(r"(?:.*://)?((?:[^/:?&]+/?)+)[?&]?.*")
        lines = [re.match(path_pattern, x).group(1) for x in lines]
        self._notice_if_lines_adjusted(self.lines, lines)
        lines = optimize_lines(lines)
        self._lines_up_to_path = lines

        lines = [re.match(r"[^\/:?]*", x).group(0) for x in lines]
        lines = optimize_lines(lines)
        self._lines_up_to_domain = lines

    def generate_all_formats(self, args: argparse.Namespace):
        """Generate rule files for all available formats"""        
        generate_format(self, ListFormat.UBLACKLIST, args)
        generate_format(self, ListFormat.ADBLOCK, args)
        generate_format(self, ListFormat.DNSMASQ, args)
        generate_format(self, ListFormat.HOSTSETC, args)
        generate_format(self, ListFormat.HOSTSIP4, args)
        generate_format(self, ListFormat.HOSTSIP6, args)


def process_full_bundle(bundle: dict, topic: str, suffix: ListSuffix, suffixes: dict[int, ListSuffix],
                        args: argparse.Namespace):
    """Take a collection of processed rulesets and bundle them together as per topic"""
    # sorting will happen just before writing to file
    try:
        process_ruleset(functools.reduce(operator.iadd, bundle.values(), []), topic, suffix, suffixes, args)
    except TypeError:
        print(f"SKIPPED: The bundle for {topic} could not be merged, because one of the parts did not contain data.")
        print(f"         Related parts: {', '.join([x.value for x in suffixes.values()])}.")


def process_ruleset(lines: list[str], topic: str, suffix: ListSuffix, suffixes: dict[int, ListSuffix],
                      args: argparse.Namespace):
    """
    Apply special treatment where necessary
    E.g. domain name needs to be added for WIKIGG rules
    """

    description = ""

    match suffix:
        case ListSuffix.WIKIGG:
            description = "Wikifarms as identified by wiki.gg Redirect (https://www.wiki.gg/redirect)"
            lines = sanitize_lines(lines)
            lines = [x + ".fandom.com" for x in lines]
        case ListSuffix.INDIEWIKI:
            description = "Wikifarms as identified by Indie Wiki Buddy (https://getindie.wiki/)"
            lines = sanitize_lines(lines)
        case ListSuffix.GAMING:
            description = "Low quality content farms related to gaming."
            lines = sanitize_lines(lines)
        case ListSuffix.TECH:
            description = "Low quality content farms related to technology"
            description += ", cyber security, cloud computing, coding, AI."
            lines = sanitize_lines(lines)
        case ListSuffix.NONE:
            description = "Random stuff."
            lines = sanitize_lines(lines)
        case ListSuffix.ALL:
            # input for aggregated lists is already sanitized
            # optimization happens in UnwantedSites
            description = f"Aggregates all {topic} lists. See variants."
        case _:
            print(f"ERROR: Suffix {suffix.value} not defined for processing.")
            return None

    unwanted = UnwantedSites(lines=lines, label=f"{topic}{suffix.value}")
    unwanted.list_variants = sorted([f"{topic}{x.value}" for x in suffixes.values()])
    unwanted.list_description = description
    unwanted.generate_all_formats(args)
    # unwanted.lines would be mostly unfiltered so it is better to return, the longest variation instead
    return unwanted.lines_up_to_path


def generate_format(unwanted: UnwantedSites, custom_format: ListFormat, args: argparse.Namespace) -> bool:
    """
    Use list of lines to generate different formats
    """

    label = unwanted.label
    written_lines = []

    match custom_format:
        case ListFormat.UBLACKLIST:
            target_file = Path(FORMAT_PATH, custom_format.value, label + ".txt")
            target_lines = ["*://*." + x.strip() + "/*" for x in unwanted.lines_up_to_path]
            header = []
            header.append("---")
            header.append(f"name: {unwanted.list_name}")
            header.append(f"description: {unwanted.list_description}")
            header.append(f"homepage: {FORMAT_URL}{custom_format.value}")
            header.append(f"variants: {', '.join(unwanted.list_variants)}")
            header.append("---")
            written_lines = write_list_from_lines(target_file, target_lines, args, header=header)
        case ListFormat.ADBLOCK:
            target_file = Path(FORMAT_PATH, custom_format.value, label + ".txt")
            target_lines = ["||" + x.strip() + "^" for x in unwanted.lines_up_to_path]
            written_lines = write_list_from_lines(target_file, target_lines, args)
        case ListFormat.DNSMASQ:
            target_file = Path(FORMAT_PATH, custom_format.value, label + ".txt")
            target_lines = ["address=/" + x.strip() + "/" for x in unwanted.lines_up_to_subdomain]
            written_lines = write_list_from_lines(target_file, target_lines, args)
        case ListFormat.HOSTSETC:
            target_file = Path(FORMAT_PATH, custom_format.value, label + ".txt")
            target_lines = ['"*://*.' + x.strip() + '/*",' for x in unwanted.lines_up_to_subdomain]
            written_lines = write_list_from_lines(target_file, target_lines, args)
        case ListFormat.HOSTSIP4:
            target_file = Path(FORMAT_PATH, custom_format.value, label + ".txt")
            target_lines = [(
                        ("0.0.0.0 " + x.strip()).replace("0 www.", "0 ")
                        + ("\n0.0.0.0 www." + x.strip()).replace("0 www.www.", "0 www.")
                    ) for x in unwanted.lines_up_to_subdomain]
            written_lines = write_list_from_lines(target_file, target_lines, args)
        case ListFormat.HOSTSIP6:
            target_file = Path(FORMAT_PATH, custom_format.value, label + ".txt")
            target_lines = [(
                        ("::1 " + x.strip()).replace("1 www.", "1 ")
                        + ("\n::1 www." + x.strip()).replace("1 www.www.", "1 www.")
                    ) for x in unwanted.lines_up_to_subdomain]
            written_lines = write_list_from_lines(target_file, target_lines, args)
        case _:
            print(f"Format {custom_format} not implemented.")
            return None

    return len(written_lines) > 0


def init_folders(args: argparse.Namespace):
    """
    docstring
    """
    result = True
    for folder in ListFormat:
        test_path = Path(FORMAT_PATH, folder.value)
        if test_path.exists():
            print(f"OK: {test_path} exists.")
        elif args.dry_run:
            print(f"WARNING: {test_path} needs to be created.")
            result = False
        elif args.init_folders:
            # only create new folders with this flag
            test_path.mkdir(parents=False, exist_ok=False)
        else:
            print(f"WARNING: {test_path} needs to be created.")
            print("Use --init-folders to do that.")
            result = False
    return result


def main():
    """
    Entry point.
    """
    parser = argparse.ArgumentParser()
    parser.add_argument("--debug", action="store_true")
    parser.add_argument("--dry-run", action="store_true")
    parser.add_argument("--init-folders", action="store_true")
    args = parser.parse_args()

    if init_folders(args=args):
        topic = "wikifarms"
        bundle = {}
        suffixes = {}
        suffixes[0] = ListSuffix.ALL
        suffixes[1] = ListSuffix.WIKIGG
        suffixes[2] = ListSuffix.INDIEWIKI

        bundle[1] = []
        bundle[1] += get_source_file_lines(Path(SOURCE_PATH, "import_from_wiki_gg.txt"))
        bundle[1] = process_ruleset(bundle[1], topic=topic, suffix=suffixes[1], suffixes=suffixes, args=args)

        bundle[2] = []
        for source_file in SOURCE_PATH.rglob("import_from_indie_wiki*"):
            bundle[2] += get_source_file_lines(source_file)
        bundle[2] = process_ruleset(bundle[2], topic=topic, suffix=suffixes[2], suffixes=suffixes, args=args)

        process_full_bundle(bundle, topic=topic, suffix=suffixes[0], suffixes=suffixes, args=args)

        topic = "contentfarms"
        bundle = {}
        suffixes = {}
        suffixes[0] = ListSuffix.ALL
        suffixes[1] = ListSuffix.GAMING
        suffixes[2] = ListSuffix.TECH

        bundle[1] = []
        bundle[1] += get_source_file_lines(Path(SOURCE_PATH, "local_lq_content_farms.gaming.txt"))
        bundle[1] = process_ruleset(bundle[1], topic=topic, suffix=suffixes[1], suffixes=suffixes, args=args)

        bundle[2] = []
        bundle[2] += get_source_file_lines(Path(SOURCE_PATH, "local_lq_content_farms.tech.txt"))
        bundle[2] = process_ruleset(bundle[2], topic=topic, suffix=suffixes[2], suffixes=suffixes, args=args)

        process_full_bundle(bundle, topic=topic, suffix=suffixes[0], suffixes=suffixes, args=args)

        topic = "unsorted"
        bundle = {}
        suffixes = {}
        suffixes[1] = ListSuffix.NONE

        bundle[1] = []
        bundle[1] += get_source_file_lines(Path(SOURCE_PATH, "local_unsorted.txt"))
        bundle[1] = process_ruleset(bundle[1], topic=topic, suffix=suffixes[1], suffixes=suffixes, args=args)

if __name__ == "__main__":
    main()
