import os
import sys
import fnmatch
import argparse
from dataclasses import dataclass
from typing import Callable, Dict, List, Optional, Tuple


@dataclass
class ConverterSpec:
    input_ext: str
    output_ext: str
    converter: Callable[[str, Optional[str]], Optional[str]]
    description: str = ""
    ignore_temp_prefixes: Tuple[str, ...] = ()

    def __post_init__(self):
        self.input_ext = normalize_ext(self.input_ext)
        self.output_ext = normalize_ext(self.output_ext)


class ConverterRegistry:
    def __init__(self):
        self._specs: Dict[Tuple[str, str], ConverterSpec] = {}
        self._import_errors: Dict[str, Exception] = {}

    def register(self, spec: ConverterSpec):
        key = (spec.input_ext, spec.output_ext)
        self._specs[key] = spec

    def set_import_error(self, module_name: str, error: Exception):
        self._import_errors[module_name] = error

    def get(self, input_ext: str, output_ext: str) -> Optional[ConverterSpec]:
        return self._specs.get((normalize_ext(input_ext), normalize_ext(output_ext)))

    def has(self, input_ext: str, output_ext: str) -> bool:
        return self.get(input_ext, output_ext) is not None

    def list_specs(self):
        return list(self._specs.values())

    def list_output_exts(self):
        return sorted({spec.output_ext for spec in self._specs.values()})

    def print_available_converters(self):
        print("Available converters:")
        for spec in sorted(self._specs.values(), key=lambda s: (s.output_ext, s.input_ext)):
            desc = f" ({spec.description})" if spec.description else ""
            print(f"  {spec.input_ext} -> {spec.output_ext}{desc}")

        if self._import_errors:
            print("\nUnavailable modules:")
            for module_name, err in self._import_errors.items():
                print(f"  {module_name}: {err}")


def normalize_ext(ext: str) -> str:
    ext = ext.strip().lower()
    if not ext.startswith("."):
        ext = "." + ext
    return ext


def build_output_path(input_path: str, output_ext: str) -> str:
    return os.path.splitext(input_path)[0] + normalize_ext(output_ext)


def should_ignore_file(filename: str, spec: ConverterSpec) -> bool:
    return any(filename.startswith(prefix) for prefix in spec.ignore_temp_prefixes)


def should_convert(input_path: str, output_path: str, force: bool = False) -> bool:
    if force:
        return True
    if not os.path.exists(output_path):
        return True
    return os.path.getmtime(input_path) > os.path.getmtime(output_path)


def parse_target_patterns(target_text: Optional[str]) -> Optional[List[str]]:
    if not target_text:
        return None
    patterns = [p.strip() for p in target_text.split(";") if p.strip()]
    return patterns or None


def matches_target(filename: str, target_patterns: Optional[List[str]]) -> bool:
    if not target_patterns:
        return True

    lower_name = filename.lower()
    for pattern in target_patterns:
        if fnmatch.fnmatch(lower_name, pattern.lower()):
            return True
    return False


def safe_import_registry(registry: ConverterRegistry):
    try:
        from docx2pdf import docx_to_pdf
    except Exception as e:
        registry.set_import_error("docx2pdf", e)
    else:
        registry.register(ConverterSpec(
            input_ext=".docx",
            output_ext=".pdf",
            converter=docx_to_pdf,
            description="Word to PDF",
        ))

    try:
        from xlsx2pdf import xlsx_to_pdf
    except Exception as e:
        registry.set_import_error("xlsx2pdf", e)
    else:
        registry.register(ConverterSpec(
            input_ext=".xlsx",
            output_ext=".pdf",
            converter=xlsx_to_pdf,
            description="Excel to PDF",
            ignore_temp_prefixes=("~$",),
        ))

    try:
        from pptx2pdf import pptx_to_pdf
    except Exception as e:
        registry.set_import_error("pptx2pdf", e)
    else:
        registry.register(ConverterSpec(
            input_ext=".pptx",
            output_ext=".pdf",
            converter=pptx_to_pdf,
            description="PowerPoint to PDF",
        ))

    try:
        from pptx2pdf_with_notes_importable import pptx_to_pdf_with_notes
    except Exception as e:
        registry.set_import_error("pptx2pdf_with_notes_importable", e)
    else:
        registry.register(ConverterSpec(
            input_ext=".pptx",
            output_ext=".notes.pdf",
            converter=pptx_to_pdf_with_notes,
            description="PowerPoint to PDF with speaker notes",
        ))

    try:
        from html2pdf_importable import html_to_pdf
    except Exception as e:
        registry.set_import_error("html2pdf_importable", e)
    else:
        registry.register(ConverterSpec(
            input_ext=".html",
            output_ext=".pdf",
            converter=html_to_pdf,
            description="HTML to PDF",
        ))
        registry.register(ConverterSpec(
            input_ext=".htm",
            output_ext=".pdf",
            converter=html_to_pdf,
            description="HTML to PDF",
        ))

    try:
        from md2pdf_importable import md_to_pdf
    except Exception as e:
        registry.set_import_error("md2pdf_importable", e)
    else:
        registry.register(ConverterSpec(
            input_ext=".md",
            output_ext=".pdf",
            converter=md_to_pdf,
            description="Markdown to PDF",
        ))

    try:
        from txt2pdf_importable import txt_to_pdf
    except Exception as e:
        registry.set_import_error("txt2pdf_importable", e)
    else:
        registry.register(ConverterSpec(
            input_ext=".txt",
            output_ext=".pdf",
            converter=txt_to_pdf,
            description="Text to PDF",
        ))

    try:
        from img2pdf_importable import image_to_pdf
    except Exception as e:
        registry.set_import_error("img2pdf_importable", e)
    else:
        for ext in (".png", ".jpg", ".jpeg", ".bmp", ".tif", ".tiff", ".webp"):
            registry.register(ConverterSpec(
                input_ext=ext,
                output_ext=".pdf",
                converter=image_to_pdf,
                description="Image to PDF",
            ))


def convert_file(input_path: str, output_ext: str, registry: ConverterRegistry, force: bool = False) -> bool:
    input_path = os.path.abspath(input_path)
    input_ext = normalize_ext(os.path.splitext(input_path)[1])

    spec = registry.get(input_ext, output_ext)
    if spec is None:
        print(f"Skipping unsupported file: '{input_path}'")
        return False

    filename = os.path.basename(input_path)
    if should_ignore_file(filename, spec):
        print(f"Skipping temporary file: '{input_path}'")
        return False

    output_path = build_output_path(input_path, output_ext)

    if should_convert(input_path, output_path, force=force):
        reason = "force=True" if force else (
            "output does not exist" if not os.path.exists(output_path) else "source is newer than output"
        )
        print(f"Converting: '{input_path}' -> '{output_path}' ({reason})")
        result = spec.converter(input_path, output_path)
        return result is not None
    else:
        print(f"Up to date. Skipping: '{input_path}'")
        return False


def walk_and_convert(
    root_dir: str,
    output_ext: str,
    registry: ConverterRegistry,
    max_level: int = -1,
    force: bool = False,
    target_patterns: Optional[List[str]] = None,
):
    if not os.path.isdir(root_dir):
        print(f"Error: Root directory '{root_dir}' does not exist.")
        return 0, 0

    root_dir_abs = os.path.abspath(root_dir)
    output_ext = normalize_ext(output_ext)

    print(f"Searching in '{root_dir_abs}'")
    print(f"Target output format: {output_ext}")
    print(f"Max level: {max_level}")
    if target_patterns:
        print(f"Target patterns: {';'.join(target_patterns)}")
    else:
        print("Target patterns: (all matching input files)")
    print("")

    converted = 0
    scanned = 0

    for dirpath, dirnames, filenames in os.walk(root_dir_abs):
        current_level = dirpath.count(os.sep) - root_dir_abs.count(os.sep)

        if max_level != -1 and current_level >= max_level:
            del dirnames[:]

        for filename in filenames:
            if not matches_target(filename, target_patterns):
                continue

            input_path = os.path.join(dirpath, filename)
            input_ext = normalize_ext(os.path.splitext(filename)[1])

            if registry.has(input_ext, output_ext):
                scanned += 1
                if convert_file(input_path, output_ext, registry, force=force):
                    converted += 1

    print("\nConversion process finished.")
    print(f"Scanned supported files: {scanned}")
    print(f"Converted files: {converted}")
    return scanned, converted


def parse_args():
    parser = argparse.ArgumentParser(
        description="Convert files in a directory tree using registered converters."
    )
    parser.add_argument("root_dir", nargs="?", default=".", help="Root directory to scan")
    parser.add_argument("output_ext", nargs="?", default=".pdf", help="Target output extension, e.g. pdf or .notes.pdf")
    parser.add_argument("--max_level", nargs="?", default="-1", help="Maximum recursion depth (-1 means unlimited)")
    parser.add_argument("--target", default=None, help='Filename patterns separated by ";" , e.g. --target="*.pptx;*.docx"')
    parser.add_argument("--force", action="store_true", help="Convert even if output is newer")
    parser.add_argument("--list", action="store_true", help="List available converters and exit")
    parser.add_argument("--no-pause", action="store_true", help="Do not wait for ENTER before exit")
    return parser.parse_args()


def main():
    args = parse_args()

    try:
        max_level = int(args.max_level)
    except ValueError:
        print(f"Error: Invalid max_level '{args.max_level}'.")
        return 1

    registry = ConverterRegistry()
    safe_import_registry(registry)

    if args.list:
        registry.print_available_converters()
        return 0

    output_ext = normalize_ext(args.output_ext)
    available_outputs = registry.list_output_exts()

    if output_ext not in available_outputs:
        print(f"Error: No converters registered for output format '{output_ext}'.")
        if available_outputs:
            print("Registered output formats:", ", ".join(available_outputs))
        registry.print_available_converters()
        return 1

    target_patterns = parse_target_patterns(args.target)

    walk_and_convert(
        root_dir=args.root_dir,
        output_ext=output_ext,
        registry=registry,
        max_level=max_level,
        force=args.force,
        target_patterns=target_patterns,
    )
    return 0


if __name__ == "__main__":
    rc = main()
    print("\nProgram execution completed.")
    if "--no-pause" not in sys.argv:
        input("\nPress ENTER to terminate>>\n")
    raise SystemExit(rc)
