#!/usr/bin/env python3
"""Leaf Index 0.1.0 — build a static file list, using only the Python standard library.
SPDX-License-Identifier: MIT
"""
import argparse
import html
import os
from pathlib import Path
import sys
from urllib.parse import quote

__version__ = "0.1.0"


def parse_args(argv=None):
    parser = argparse.ArgumentParser(description="Generate a linked HTML list of a directory. Hidden entries and symlinks are skipped.")
    parser.add_argument("directory", type=Path, help="directory to list")
    parser.add_argument("--output", type=Path, help="write a NEW file instead of stdout; existing files are never overwritten")
    parser.add_argument("--recursive", action="store_true", help="include ordinary subdirectories")
    parser.add_argument("--max-depth", type=int, default=3, help="maximum subdirectory depth with --recursive (default: 3; 0 = root files only)")
    parser.add_argument("--extensions", help="comma-separated file suffixes, for example .md,.pdf; case-insensitive")
    parser.add_argument("--title", default="File index", help="HTML page title")
    parser.add_argument("--version", action="version", version="Leaf Index " + __version__)
    args = parser.parse_args(argv)
    if args.max_depth < 0:
        parser.error("--max-depth must be zero or greater")
    if args.directory.is_symlink():
        parser.error("the input directory must not be a symlink")
    args.directory = args.directory.resolve()
    if not args.directory.is_dir():
        parser.error("directory does not exist or is not a directory")
    if args.output is not None:
        if args.output.is_symlink():
            parser.error("output must not be a symlink")
        args.output = args.output.absolute()
        if args.output.exists():
            parser.error("output already exists; choose a new filename")
        if not args.output.parent.is_dir():
            parser.error("output parent directory does not exist")
    return args


def collect_files(root, recursive=False, max_depth=3, extensions=None):
    """Return sorted regular files. Never traverse hidden directories or symlinks."""
    result = []

    def walk(folder, depth):
        with os.scandir(folder) as entries:
            entries = sorted(entries, key=lambda e: (e.name.casefold(), e.name))
        for entry in entries:
            if entry.name.startswith(".") or entry.is_symlink():
                continue
            if entry.is_dir(follow_symlinks=False):
                if recursive and depth < max_depth:
                    walk(Path(entry.path), depth + 1)
            elif entry.is_file(follow_symlinks=False):
                file = Path(entry.path)
                if extensions is None or file.suffix.casefold() in extensions:
                    result.append(file)

    walk(root, 0)
    return sorted(result, key=lambda f: (f.relative_to(root).as_posix().casefold(), f.relative_to(root).as_posix()))


def render(files, root, link_base, title):
    rows = []
    for file in files:
        label = file.relative_to(root).as_posix()
        relative = Path(os.path.relpath(file, link_base)).as_posix()
        # Prefix relative URLs, including names containing a colon, with './'.
        href = relative if relative.startswith("../") else "./" + relative
        href = quote(href, safe="/.")
        rows.append('<li><a href="' + html.escape(href, quote=True) + '">' + html.escape(label) + '</a></li>')
    content = '<ul>' + ''.join(rows) + '</ul>' if rows else '<p class="empty">No matching files.</p>'
    title = html.escape(title)
    return '''<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>''' + title + '''</title><style>
*{box-sizing:border-box}body{max-width:780px;margin:0 auto;padding:40px 24px;font:16px/1.7 system-ui,sans-serif;color:#27343b;background:#f8faf9}h1{font-size:28px;line-height:1.3;overflow-wrap:anywhere}p{color:#5f6d73}ul{list-style:none;padding:0;border-top:1px solid #d9e2df}li{border-bottom:1px solid #d9e2df}a{display:block;padding:12px 2px;color:#15684d;overflow-wrap:anywhere}footer{font-size:12px;color:#5f6d73;margin-top:30px}
</style></head><body><main><h1>''' + title + '</h1><p>' + str(len(files)) + ' file' + ('' if len(files) == 1 else 's') + '</p>' + content + '''</main><footer>Generated with Leaf Index · static HTML</footer></body></html>
'''


def main(argv=None):
    args = parse_args(argv)
    extensions = None
    if args.extensions:
        extensions = {('.' + x.strip().lstrip('.')).casefold() for x in args.extensions.split(',') if x.strip()}
    try:
        files = collect_files(args.directory, args.recursive, args.max_depth, extensions)
        # Stdout assumes the saved page will live in the input directory.
        link_base = args.output.parent.resolve() if args.output else args.directory
        page = render(files, args.directory, link_base, args.title)
        if args.output:
            with args.output.open('x', encoding='utf-8', newline='\n') as output:
                output.write(page)
            print('Wrote ' + str(args.output) + ' (' + str(len(files)) + ' files)', file=sys.stderr)
        else:
            sys.stdout.write(page)
    except (OSError, ValueError) as error:
        print('leafindex: ' + str(error), file=sys.stderr)
        return 1
    return 0


if __name__ == '__main__':
    raise SystemExit(main())
