#!/usr/bin/env python
import os
import pathlib
import shutil
import sys
from collections.abc import Callable

import yaml

from bs4 import BeautifulSoup


def get_nav_from_selector(soup: BeautifulSoup, selector: str, docs_dir: str):
    """
    Generates navigation from template file
    """
    navigation = []
    header_name = ""
    for anchor in soup.select(selector):
        classes = anchor.get("class", [])
        if classes is None:
            continue

        if "disabled" in classes:
            header_name = anchor.text
            navigation.append({header_name: []})

        href = anchor.get("href", "")
        if href is None:
            continue

        href = str(href)
        if href == "#":
            continue

        url = href.replace("{{NAME}}", "")
        filename = url.replace("http://progrium.viewdocs.io/dokku/", "")
        filename = filename.replace("http://dokku.viewdocs.io/dokku/", "")
        filename = filename.strip("/") + ".md"
        filename = filename.removeprefix("dokku/")

        ignore_errors_for = [
            "getting-started/installation.md",
            "getting-started/upgrading.md",
        ]
        if not os.path.exists(docs_dir + "/" + filename):
            if filename not in ignore_errors_for:
                print("error fetching markdown file:", filename)
                continue

        for nav in navigation:
            if header_name in nav:
                if filename == "getting-started/installation.md":
                    child_dir = "getting-started/install/"
                    children = os.listdir(docs_dir + "/" + child_dir)
                    children = [child_dir + c for c in children]
                    children.sort()
                    children.insert(0, "getting-started/advanced-installation.md")
                    if os.path.exists(docs_dir + "/" + filename):
                        children.insert(0, filename)
                    else:
                        children.insert(0, "getting-started/installation/index.md")
                    nav[header_name].append(
                        {
                            "Getting Started with Dokku": children,
                        }
                    )
                    continue

                if filename == "getting-started/upgrading.md":
                    child_dir = "appendices/"
                    children = os.listdir(docs_dir + "/" + child_dir)

                    children.sort(
                        key=lambda x: list(map(int, x.split("-")[0].split("."))),
                        reverse=True,
                    )
                    children = [child_dir + c for c in children]
                    if os.path.exists(docs_dir + "/" + filename):
                        children.insert(0, filename)
                    else:
                        children.insert(0, "getting-started/upgrading/index.md")
                    nav[header_name].append(
                        {
                            "Upgrading": children,
                        }
                    )
                    continue

                nav[header_name].append(filename)
    return navigation


def generate_nav(src: str, dest: str) -> None:
    """
    Writes out navigation information to the mkdocs yaml file
    """
    navigation = []
    repo_dir = pathlib.Path(__file__).parent.parent.resolve()
    docs_dir = str(repo_dir) + "/docs"
    with open(docs_dir + "/template.html", encoding="utf-8") as response:
        soup = BeautifulSoup(response, "html.parser")
        selectors = [
            ".container .row .list-group a",
            ".container-fluid .row .list-group a",
        ]
        for selector in selectors:
            navigation = get_nav_from_selector(soup, selector, docs_dir)
            if len(navigation) > 0:
                break

        if len(navigation) == 0:
            print("No navigation found")
            sys.exit(1)

    with open(src, encoding="utf-8") as handler:
        data = yaml.unsafe_load(handler)
        data["nav"] = [
            {"Docs": navigation},
            {"Pro": "https://pro.dokku.com/docs/getting-started/"},
            {"Blog": "https://dokku.com/blog/"},
            {"Tutorials": "https://dokku.com/tutorials/"},
            {
                "Purchase Dokku Pro": "https://dokku.dpdcart.com/cart/add?product_id=217344&method_id=236878"
            },
        ]

    with open(dest, mode="wt", encoding="utf-8") as handler:
        yaml.dump(data, handler)


def modify_content_noop(lines: list[str], _) -> tuple[list[str], bool]:
    """
    Simply returns the lines as is
    """
    modified = False
    updated_lines = []
    for line in lines:
        updated_lines.append(line)
    return updated_lines, modified


def modify_content_links(lines: list[str], filename: str) -> tuple[list[str], bool]:
    """
    Modifies links to be relative instead of absolute
    """
    filename = filename.replace("/usr/src/source/docs/", "")
    parts = filename.split("/")
    parts.pop()
    replacement = "](" + "/".join([".." for _ in parts]) + "/"
    modified = False
    updated_lines = []
    for line in lines:
        if "](/docs/" in line:
            line = line.replace("](/docs/", replacement)
            modified = True

        updated_lines.append(line)
    return updated_lines, modified


def modify_content_stripspace(lines: list[str], _) -> tuple[list[str], bool]:
    """
    Removes trailing whitespace from each line
    """
    modified = False
    updated_lines = []
    for line in lines:
        line = line.rstrip()
        updated_lines.append(line)
    return updated_lines, modified


def modify_content_inject_newlines(lines: list[str], _) -> tuple[list[str], bool]:
    """
    Ensures every line has a trailing newline character
    """
    modified = False
    updated_lines = []
    for line in lines:
        updated_lines.append(line.rstrip() + "\n")
    return updated_lines, modified


def is_github_new(line: str, next_line: str | None) -> bool:
    """
    Checks if a given line is a github "new as of" admonition
    """
    if not next_line:
        return False

    return line.startswith("> [!IMPORTANT]") and "new as of" in next_line.lower()


def is_github_note(line: str) -> bool:
    """
    Checks if a given line is a github "note" admonition
    """
    return line.startswith("> [!NOTE]")


def is_github_warning(line: str) -> bool:
    """
    Checks if a given line is a github "warning" admonition
    """
    return line.startswith("> [!WARNING]")


def is_info(line: str) -> bool:
    """
    Checks if a given line is an "info" admonition
    """
    return line.startswith("> ")


def is_new(line: str) -> bool:
    """
    Checks if a given line is a "new as of" admonition
    """
    return line.startswith("> ") and "new as of" in line.lower()


def is_note(line: str) -> bool:
    """
    Checks if a given line is a "note" admonition
    """
    return line.startswith("> Note:")


def is_warning(line: str) -> bool:
    """
    Checks if a given line is a "warning" admonition
    """
    return line.startswith("> Warning:")


def modify_content_admonition(lines: list[str], _) -> tuple[list[str], bool]:
    """
    Applies adminition info to each line in the output
    """
    modified = False
    updated_lines = []
    admonition_lines = []
    is_admonition = False
    replace_new_in_next_line = False
    for index, line in enumerate(lines):
        next_line: str | None = None
        if index + 1 < len(lines):
            next_line = lines[index + 1]

        if replace_new_in_next_line:
            line = line.replace("New as of", "Introduced in")
            line = line.replace("new as of", "introduced in")
            replace_new_in_next_line = False

        if is_github_new(line, next_line):
            line = line.replace("> [!IMPORTANT]", '!!! tip "New"')
            is_admonition = True
            admonition_lines.append(line)
            admonition_lines.append("")
            replace_new_in_next_line = True
        elif is_github_note(line):
            line = line.replace("> [!NOTE]", '!!! note "Note"')
            is_admonition = True
            admonition_lines.append(line)
            admonition_lines.append("")
        elif is_github_warning(line):
            line = line.replace("> [!WARNING]", '!!! warning "Warning"')
            is_admonition = True
            admonition_lines.append(line)
            admonition_lines.append("")
        elif is_new(line):
            print("is_new")
            line = line.replace("> ", '!!! tip "New"\n\n    ')
            line = line.replace("New as of", "Introduced in")
            line = line.replace("new as of", "introduced in")
            is_admonition = True
            admonition_lines.append(line)
        elif is_note(line):
            print("is_note")
            line = line.replace("> Note: ", '!!! note "Note"\n\n    ')
            is_admonition = True
            admonition_lines.append(line)
        elif is_warning(line):
            print("is_warning")
            line = line.replace("> Warning: ", '!!! warning "Warning"\n\n    ')
            is_admonition = True
            admonition_lines.append(line)
        elif is_info(line):
            if not is_admonition:
                line = line.replace("> ", '!!! info "Info"\n\n    ')
            elif line in [">", "> "]:
                line = ""
            else:
                line = "    " + line.removeprefix("> ")
            is_admonition = True
            admonition_lines.append(line)
        elif is_admonition and line in [">", "> "]:
            line = ""
            admonition_lines.append(line)
        elif is_admonition and line.startswith("> "):
            line = "    " + line.removeprefix("> ")
            admonition_lines.append(line)
        elif line == "":
            is_admonition = False
            if len(admonition_lines) > 0:
                modified = True
                updated_lines.extend(admonition_lines)
                admonition_lines = []
            updated_lines.append("")
        else:
            updated_lines.append(line)
    return updated_lines, modified


def is_shell_codeblock_start(line: str) -> bool:
    """
    Checks to see if a line starts a codeblock
    """
    return line == "```shell"


def modify_content_terminal_example(lines: list[str], _) -> tuple[list[str], bool]:
    """
    Modifies content so that terminal output is shown appropriately
    """
    modified = False
    updated_lines = []
    command_block = []
    example_block = []
    in_command_block = False
    in_example_block = False
    previous_block = ""
    next_line_must_be = None
    for line in lines:
        if is_shell_codeblock_start(line):
            command_block.append(line)
            modified = True
            in_command_block = True
            continue
        elif in_command_block:
            command_block.append(line)
            if line == "```":
                in_command_block = False
                previous_block = "command_block"
                next_line_must_be = ""
            continue
        elif line == "```":
            if previous_block == "":
                updated_lines.append(line)
                continue
            if previous_block == "command_block":
                example_block.append(line)

                if in_example_block:
                    previous_block = ""
                    in_example_block = False
                    updated_lines.append('=== "Shell"')
                    updated_lines.append("")
                    for command_line in command_block:
                        updated_lines.append(f"    {command_line}")
                    command_block = []

                    updated_lines.append("")
                    updated_lines.append('=== "Output"')
                    updated_lines.append("")
                    for example_line in example_block:
                        updated_lines.append(f"    {example_line}")
                    example_block = []
                else:
                    in_example_block = True
                continue
        elif previous_block == "command_block":
            if next_line_must_be is None:
                if in_example_block:
                    example_block.append(line)
                else:
                    updated_lines.extend(command_block)
                    updated_lines.append("")
                    updated_lines.append(line)
                    command_block = []
                    previous_block = ""
                continue
            if next_line_must_be == "":
                if line == "":
                    next_line_must_be = None
                    continue

        updated_lines.append(line)

    if len(command_block) > 0:
        updated_lines.extend(command_block)

    return updated_lines, modified


def update_markdown(src):
    """
    Updates all markdown files to be mkdocs compatible
    """
    markdown_files = []
    allowed_extensions = [".md"]
    for subdir, _, files in os.walk(src):
        for file in files:
            ext = os.path.splitext(file)[-1].lower()
            file_path = os.path.join(subdir, file)
            if ext in allowed_extensions:
                markdown_files.append(file_path)

    modifiers: list[Callable[[list[str], str], tuple[list[str], bool]]] = [
        modify_content_noop,
        modify_content_links,
        modify_content_stripspace,
        modify_content_admonition,
        modify_content_terminal_example,
        modify_content_inject_newlines,
    ]

    for file in markdown_files:
        is_modified = False
        lines = []
        with open(file, "r", encoding="utf-8") as handler:
            lines = handler.readlines()
            for modifier in modifiers:
                lines, modified = modifier(lines, file)
                if modified:
                    is_modified = True

        if is_modified:
            with open(file, "w", encoding="utf-8") as handler:
                handler.writelines(lines)


def main():
    """
    Main command that performs doc manipulation
    """
    print("----> Copying mkdocs.yml")
    print("      Generating navigation")
    generate_nav("/usr/src/source/mkdocs.yml", "/usr/src/app/mkdocs.yml")

    print("----> Copying docs folder")
    update_markdown(
        "/usr/src/source/docs",
    )

    if os.path.exists("/usr/src/app/docs"):
        print("      Removing old docs folder")
        shutil.rmtree("/usr/src/app/docs")

    print("      Performing copy")
    shutil.copytree("/usr/src/source/docs", "/usr/src/app/docs")


if __name__ == "__main__":
    main()
