import re
import subprocess
from pathlib import Path

from jinja2 import Template
from pydantic2ts import generate_typescript_defs
from utils import log

# ============================================================

template = """// This Code is auto generated by gen_ts_types.py
{% for name in global %}import type {{ name }} from "@/components/global/{{ name }}.vue";
{% endfor %}{% for name in layout %}import type {{ name }} from "@/components/layout/{{ name }}.vue";
{% endfor %}
declare module "vue" {
  export interface GlobalComponents {
    // Global Components
{% for name in global -%}
    {{ "    " }}{{ name }}: typeof {{ name }};
{% endfor -%}
    {{ "    " }}// Layout Components
{% for name in layout -%}
    {{ "    " }}{{ name }}: typeof {{ name }};
{% endfor -%}{{ "  }"}}
}

export {};

"""

CWD = Path(__file__).parent
PROJECT_DIR = Path(__file__).parent.parent.parent


def generate_global_components_types() -> None:
    destination_file = PROJECT_DIR / "frontend" / "app" / "types" / "components.d.ts"

    component_paths = {
        "global": PROJECT_DIR / "frontend" / "app" / "components" / "global",
        "layout": PROJECT_DIR / "frontend" / "app" / "components" / "Layout",
    }

    def render_template(template: str, data: dict) -> str | None:
        tmpl = Template(template)
        return tmpl.render(**data)

    def build_data() -> dict:
        data = {}
        for name, path in component_paths.items():
            components = [component.stem for component in path.glob("*.vue")]
            components.sort()
            data[name] = components

        return data

    def write_template(text: str) -> None:
        destination_file.write_text(text)

    text = render_template(template, build_data())

    if text:
        write_template(text)


# ============================================================
# Pydantic To Typescript Generator


def generate_typescript_types() -> None:  # noqa: C901
    def contains_number(s: str) -> bool:
        return bool(re.search(r"\d", s))

    def remove_numbers(s: str) -> str:
        return re.sub(r"\d", "", s)

    def extract_type_name(line: str) -> str:
        # Looking for "export type EnumName = enumVal1 | enumVal2 | ..."
        if not (line.startswith("export type") and "=" in line):
            return ""

        return line.split(" ")[2]

    def extract_property_type_name(line: str) -> str:
        # Looking for " fieldName: FieldType;" or " fieldName: FieldType & string;"
        if not (line.startswith("  ") and ":" in line):
            return ""

        return line.split(":")[1].strip().split(";")[0]

    def extract_interface_name(line: str) -> str:
        # Looking for "export interface InterfaceName {"
        if not (line.startswith("export interface") and "{" in line):
            return ""

        return line.split(" ")[2]

    def is_comment_line(line: str) -> bool:
        s = line.strip()
        return s.startswith("/*") or s.startswith("*")

    def clean_output_file(file: Path) -> None:
        """
        json2ts generates duplicate types off of our enums and appends a number to the end of the type name.
        Our Python code (hopefully) doesn't have any duplicate enum names, or types with numbers in them,
        so we can safely remove the numbers.

        To do this, we read the output line-by-line and replace any type names that contain numbers with
        the same type name, but without the numbers.

        Note: the issue arrises from the JSON package json2ts, not the Python package pydantic2ts,
        otherwise we could just fix pydantic2ts.
        """

        # First pass: build a map of type names to their numberless counterparts and lines to skip
        replacement_map = {}
        lines_to_skip = set()
        wait_for_semicolon = False
        wait_for_close_bracket = False
        skip_comments = False
        with open(file) as f:
            for i, line in enumerate(f.readlines()):
                if wait_for_semicolon:
                    if ";" in line:
                        wait_for_semicolon = False
                    lines_to_skip.add(i)
                    continue
                if wait_for_close_bracket:
                    if "}" in line:
                        wait_for_close_bracket = False
                    lines_to_skip.add(i)
                    continue

                if type_name := extract_type_name(line):
                    if not contains_number(type_name):
                        continue

                    replacement_map[type_name] = remove_numbers(type_name)
                    if ";" not in line:
                        wait_for_semicolon = True
                    lines_to_skip.add(i)

                elif type_name := extract_interface_name(line):
                    if not contains_number(type_name):
                        continue

                    replacement_map[type_name] = remove_numbers(type_name)
                    if "}" not in line:
                        wait_for_close_bracket = True
                    lines_to_skip.add(i)

                elif skip_comments and is_comment_line(line):
                    lines_to_skip.add(i)

                # we've passed the opening comments and empty line at the header
                elif not skip_comments and not line.strip():
                    skip_comments = True

        # Second pass: rewrite or remove lines as needed.
        # We have to do two passes here because definitions don't always appear in the same order as their usage.
        lines = []
        with open(file) as f:
            for i, line in enumerate(f.readlines()):
                if i in lines_to_skip:
                    continue

                if type_name := extract_property_type_name(line):
                    if type_name in replacement_map:
                        line = line.replace(type_name, replacement_map[type_name])

                lines.append(line)

        with open(file, "w") as f:
            f.writelines(lines)

    def path_to_module(path: Path):
        str_path: str = str(path)

        str_path = str_path.removeprefix(str(PROJECT_DIR))
        str_path = str_path.removeprefix("/")
        str_path = str_path.replace("/", ".")

        return str_path

    schema_path = PROJECT_DIR / "mealie" / "schema"
    types_dir = PROJECT_DIR / "frontend" / "app" / "lib" / "api" / "types"

    ignore_dirs = ["__pycache__", "static", "_mealie"]

    skipped_files: list[Path] = []
    skipped_dirs: list[Path] = []
    failed_modules: list[Path] = []

    out_paths: list[Path] = []
    for module in schema_path.iterdir():
        if module.is_dir() and module.stem in ignore_dirs:
            skipped_dirs.append(module)
            continue

        if not module.is_dir() or not module.joinpath("__init__.py").is_file():
            skipped_files.append(module)
            continue

        ts_out_name = module.name.replace("_", "-") + ".ts"
        out_path = types_dir.joinpath(ts_out_name)

        try:
            path_as_module = path_to_module(module)
            generate_typescript_defs(path_as_module, str(out_path), exclude=("MealieModel"))  # type: ignore
            clean_output_file(out_path)
            out_paths.append(out_path)
        except Exception:
            failed_modules.append(module)
            log.exception(f"Module Error: {module}")

    # Run ESLint --fix on the files to clean up any formatting issues
    subprocess.run(
        ["yarn", "lint", "--fix", *(str(path) for path in out_paths)],
        check=True,
        cwd=PROJECT_DIR / "frontend",
    )

    log.debug("\n📁 Skipped Directories:")
    for skipped_dir in skipped_dirs:
        log.debug(f"   📁 {skipped_dir.name}")

    log.debug("📄 Skipped Files:")
    for f in skipped_files:
        log.debug(f"   📄 {f.name}")

    if len(failed_modules) > 0:
        log.error("❌ Failed Modules:")
        for f in failed_modules:
            log.error(f"   ❌ {f.name}")


def main():
    log.debug("\n-- Starting Global Components Generator --")
    generate_global_components_types()

    log.debug("\n-- Starting Pydantic To Typescript Generator --")
    generate_typescript_types()


if __name__ == "__main__":
    main()
