"""Document selection and template matching utilities."""

from __future__ import annotations

import difflib
from pathlib import Path, PurePosixPath, PureWindowsPath
from typing import Any


def normalize_path_string(path_str: str) -> str:
    """Normalize a Windows or POSIX path string into POSIX form."""
    if "\\" in path_str:
        return PureWindowsPath(path_str).as_posix()
    return PurePosixPath(path_str).as_posix()


def _path_parts(path_str: str) -> PurePosixPath:
    return PurePosixPath(normalize_path_string(path_str))


def _stem_candidates(stem: str) -> list[str]:
    candidates = [stem]
    current = stem
    while True:
        last_dash = current.rfind("-")
        last_underscore = current.rfind("_")
        split_index = max(last_dash, last_underscore)
        if split_index <= 0:
            break
        current = current[:split_index]
        if current not in candidates:
            candidates.append(current)
    return candidates


def _is_recording_artifact(path: Path) -> bool:
    name = path.name.casefold()
    return name.endswith(".recording.jsonl.gz") or name.endswith(".jsonl.gz") or name.endswith(".jsonl")


def get_normalized_document_key(doc_path: str) -> tuple[str, str]:
    """Return a normalized filename and suffix pair for a document path."""
    path = _path_parts(doc_path)
    return path.name, path.suffix


def group_documents_by_name(docs: list[str]) -> dict[tuple[str, str], list[str]]:
    """Group recorder document paths by normalized filename and suffix."""
    groups: dict[tuple[str, str], list[str]] = {}
    for doc in docs:
        groups.setdefault(get_normalized_document_key(doc), []).append(doc)
    return groups


def get_recorded_documents(events: tuple[dict[str, Any], ...]) -> list[str]:
    """Return sorted unique document paths found in the recording."""
    return sorted(
        {
            str(event["document"])
            for event in events
            if event.get("document") is not None
        }
    )


def _matching_documents(recorded_docs: list[str], requested: str) -> list[str]:
    target = _path_parts(requested)
    exact_matches = [
        doc for doc in recorded_docs if normalize_path_string(doc) == target.as_posix()
    ]
    if exact_matches:
        return exact_matches

    by_name = [
        doc for doc in recorded_docs if _path_parts(doc).name.casefold() == target.name.casefold()
    ]
    if by_name:
        return by_name

    by_stem = [
        doc
        for doc in recorded_docs
        if _path_parts(doc).stem.casefold() == target.stem.casefold()
        and (
            not target.suffix
            or _path_parts(doc).suffix.casefold() == target.suffix.casefold()
        )
    ]
    if by_stem:
        return by_stem

    if target.suffix:
        same_suffix = [
            doc for doc in recorded_docs if _path_parts(doc).suffix.casefold() == target.suffix.casefold()
        ]
        if len(same_suffix) == 1:
            return same_suffix
        if same_suffix:
            names = [_path_parts(doc).name for doc in same_suffix]
            match = difflib.get_close_matches(target.name, names, n=1, cutoff=0.4)
            if match:
                return [doc for doc in same_suffix if _path_parts(doc).name == match[0]]

    return []


def filter_events_by_document(
    events: tuple[dict[str, Any], ...],
    document: str | None,
) -> tuple[dict[str, Any], ...]:
    """Filter events to an exact document path match."""
    if document is None:
        return events

    target = normalize_path_string(document)
    return tuple(
        event
        for event in events
        if normalize_path_string(str(event.get("document", ""))) == target
    )


def filter_events_by_document_with_rename_handling(
    events: tuple[dict[str, Any], ...],
    document: str | None,
) -> tuple[dict[str, Any], ...]:
    """Filter events to the best matching document, allowing renamed paths."""
    if document is None:
        return events

    recorded_docs = get_recorded_documents(events)
    matching_docs = _matching_documents(recorded_docs, document)
    if not matching_docs:
        return ()

    normalized_matches = {normalize_path_string(doc) for doc in matching_docs}
    return tuple(
        event
        for event in events
        if normalize_path_string(str(event.get("document", ""))) in normalized_matches
    )


def find_matching_template(template_dir: Path, document_path: str) -> Path | None:
    """Find the template file that best matches a recorded document."""
    if not template_dir.is_dir():
        return None

    candidates = sorted(
        path
        for path in template_dir.iterdir()
        if path.is_file() and not _is_recording_artifact(path)
    )
    if not candidates:
        return None

    document = _path_parts(document_path)
    exact = template_dir / document.name
    if exact.is_file() and not _is_recording_artifact(exact):
        return exact

    suffix_candidates = candidates
    if document.suffix:
        suffix_candidates = [
            path for path in candidates if path.suffix.casefold() == document.suffix.casefold()
        ]

    for stem in _stem_candidates(document.stem):
        same_stem = [
            path
            for path in suffix_candidates
            if path.stem == stem
        ]
        if same_stem:
            return same_stem[0]

    fuzzy_candidates = suffix_candidates if suffix_candidates else candidates
    if document.suffix and not suffix_candidates:
        return None

    names = [path.name for path in fuzzy_candidates]
    name_match = difflib.get_close_matches(document.name, names, n=1, cutoff=0.4)
    if name_match:
        return template_dir / name_match[0]

    stems = [path.stem for path in fuzzy_candidates]
    stem_match = difflib.get_close_matches(document.stem, stems, n=1, cutoff=0.4)
    if stem_match:
        for path in fuzzy_candidates:
            if path.stem == stem_match[0]:
                return path

    return None


def resolve_document(
    recorded_docs: list[str],
    template_base: Path | None,
    document_override: str | None,
) -> str | None:
    """Choose the document to process from a recording."""
    if not recorded_docs:
        return None

    if document_override:
        matches = _matching_documents(recorded_docs, document_override)
        return matches[0] if matches else document_override

    if len(recorded_docs) == 1:
        return recorded_docs[0]

    if template_base and template_base.is_file():
        matches = _matching_documents(recorded_docs, str(template_base))
        if matches:
            return matches[0]

    if template_base and template_base.suffix:
        same_suffix = [
            doc for doc in recorded_docs if _path_parts(doc).suffix == template_base.suffix
        ]
        if len(same_suffix) == 1:
            return same_suffix[0]

    python_docs = [doc for doc in recorded_docs if _path_parts(doc).suffix == ".py"]
    if len(python_docs) == 1:
        return python_docs[0]

    return recorded_docs[0]


def resolve_template_file(
    template_path: Path | None,
    template_dir: Path | None,
    document_path: str | None,
) -> tuple[str, Path | None]:
    """Load the template text for a document from a file or template directory."""
    if template_dir is not None:
        if document_path is None:
            return "", None
        match = find_matching_template(template_dir, document_path)
        if match is None:
            return "", None
        return match.read_text(), match

    if template_path is None:
        return "", None
    if not template_path.exists():
        raise FileNotFoundError(f"Template file not found: {template_path}")
    return template_path.read_text(), template_path
