#!/usr/bin/env python3
# merge_text_files.py

from __future__ import annotations

import argparse
import fnmatch
from pathlib import Path


"""
python merge_text_files.py C:\work\tkProg -o launcher_docs_merged.txt -r -d 5 -p "*.txt" "*.md"
python merge_text_files.py C:\work\tkProg -o launcher_docs_merged.txt -r -d 5 -p "*.txt;*.md"

"""


FILE_START = "===== FILE START ====="
CONTENT_START = "===== CONTENT ====="
FILE_END = "===== FILE END ====="


def guess_type(path: Path) -> str:
    ext = path.suffix.lower()
    if ext == ".py":
        return "python"
    if ext == ".ini":
        return "ini"
    if ext == ".md":
        return "md"
    if ext == ".txt":
        return "txt"
    return ext.lstrip(".") or "unknown"


def split_patterns(patterns: list[str]) -> list[str]:
    result: list[str] = []
    for item in patterns:
        for part in item.split(";"):
            part = part.strip()
            if part:
                result.append(part)
    return result


def is_within_depth(path: Path, root: Path, max_depth: int) -> bool:
    if max_depth < 0:
        return True

    rel = path.relative_to(root)
    depth = len(rel.parts) - 1
    return depth <= max_depth


def match_any(path: Path, root: Path, patterns: list[str]) -> bool:
    rel_posix = path.relative_to(root).as_posix()
    name = path.name

    return any(
        fnmatch.fnmatch(name, pat) or fnmatch.fnmatch(rel_posix, pat)
        for pat in patterns
    )


def collect_files(
    root: Path,
    patterns: list[str],
    recursive: bool,
    max_depth: int,
    output_path: Path | None,
) -> list[Path]:
    iterator = root.rglob("*") if recursive else root.glob("*")

    files: list[Path] = []
    for path in iterator:
        if not path.is_file():
            continue

        if output_path is not None:
            try:
                if path.resolve() == output_path.resolve():
                    continue
            except FileNotFoundError:
                pass

        if not is_within_depth(path, root, max_depth):
            continue

        if match_any(path, root, patterns):
            files.append(path)

    return sorted(files, key=lambda p: p.relative_to(root).as_posix().lower())


def read_text_file(path: Path, encoding: str) -> str:
    try:
        return path.read_text(encoding=encoding)
    except UnicodeDecodeError:
        return path.read_text(encoding="utf-8-sig", errors="replace")


def merge_files(
    root: Path,
    files: list[Path],
    output_path: Path,
    encoding: str,
    blank_lines: int,
) -> None:
    separator = "\n" * blank_lines

    with output_path.open("w", encoding=encoding, newline="\n") as out:
        for i, path in enumerate(files):
            rel_path = path.relative_to(root).as_posix()
            file_type = guess_type(path)
            text = read_text_file(path, encoding)

            if i > 0:
                out.write(separator)

            out.write(f"{FILE_START}\n")
            out.write(f"path: {rel_path}\n")
            out.write(f"type: {file_type}\n")
            out.write(f"tags: []\n")
            out.write(f"{CONTENT_START}\n")
            out.write(text)

            if text and not text.endswith("\n"):
                out.write("\n")

            out.write(f"{FILE_END}\n")


def main() -> None:
    parser = argparse.ArgumentParser(
        description="Merge .txt/.md/etc files with path metadata."
    )

    parser.add_argument(
        "root",
        help="Root directory to search.",
    )

    parser.add_argument(
        "-o",
        "--output",
        default="merged_files.txt",
        help="Output merged file path. Default: merged_files.txt",
    )

    parser.add_argument(
        "-p",
        "--patterns",
        nargs="+",
        default=["*.txt", "*.md"],
        help='Wildcard patterns. Example: -p "*.txt" "*.md" or -p "*.txt;*.md"',
    )

    parser.add_argument(
        "-r",
        "--recursive",
        action="store_true",
        help="Search recursively.",
    )

    parser.add_argument(
        "-d",
        "--max-depth",
        type=int,
        default=-1,
        help="Maximum directory depth from root. -1 means unlimited. Default: -1",
    )

    parser.add_argument(
        "--encoding",
        default="utf-8",
        help="Text encoding for input/output. Default: utf-8",
    )

    parser.add_argument(
        "--blank-lines",
        type=int,
        default=5,
        help="Blank lines between FILE END and next FILE START. Default: 5",
    )

    args = parser.parse_args()

    root = Path(args.root).expanduser().resolve()
    output_path = Path(args.output).expanduser().resolve()
    patterns = split_patterns(args.patterns)

    if not root.exists():
        raise FileNotFoundError(f"Root directory not found: {root}")

    if not root.is_dir():
        raise NotADirectoryError(f"Root is not a directory: {root}")

    if args.blank_lines < 5:
        raise ValueError("--blank-lines must be 5 or more.")

    files = collect_files(
        root=root,
        patterns=patterns,
        recursive=args.recursive,
        max_depth=args.max_depth,
        output_path=output_path,
    )

    output_path.parent.mkdir(parents=True, exist_ok=True)

    merge_files(
        root=root,
        files=files,
        output_path=output_path,
        encoding=args.encoding,
        blank_lines=args.blank_lines,
    )

    print(f"Merged {len(files)} files")
    print(f"Output: {output_path}")


if __name__ == "__main__":
    main()
    input("\nPress ENTER to terminate>>\n")
    