"""Directory merge utilities for the ``prompt merge`` command.

This module keeps merge-specific logic isolated from generic input
resolution in ``inputs.py``.
"""

import json
from enum import StrEnum
from pathlib import Path
from typing import Any

from dotpromptz.inputs import infer_schema
from dotpromptz.yamlio import YamlError, dump_yaml, load_yaml

SUPPORTED_MERGE_EXTENSIONS = frozenset({'.json', '.yaml', '.yml', '.txt'})
_FILENAME_FIELD = 'FILENAME'


class MergeFormat(StrEnum):
    """Supported normalized input formats for merge."""

    JSON = 'json'
    YAML = 'yaml'
    TEXT = 'text'


def merge_directory(
    directory: Path,
    *,
    pattern: str | None = None,
    with_filename: bool = False,
    force: bool = False,
) -> Path:
    """Merge files in a directory into a single output file.

    Args:
        directory: Target directory whose files will be merged.
        pattern: Optional glob pattern used for matching files.
            When omitted, only top-level supported files are matched.
        with_filename: Whether to inject ``FILENAME`` into each output item.
        force: Whether to overwrite an existing output file.

    Returns:
        Path to the merged output file.

    Raises:
        ValueError: If path/format/schema validation fails.
        OSError: If writing output fails.
    """
    files = _collect_files(directory, pattern)
    fmt = _detect_format(files)
    output_path = _resolve_output_path(directory, fmt)

    if output_path.exists() and not force:
        raise ValueError(f'Output file already exists: {output_path}. Use --force to overwrite.')

    if fmt in {MergeFormat.JSON, MergeFormat.YAML}:
        merged_records = _merge_structured_files(files, fmt, with_filename)
        if fmt == MergeFormat.JSON:
            _write_jsonl(output_path, merged_records)
        else:
            _write_yaml(output_path, merged_records)
    else:
        merged_text_records = _merge_text_files(files, with_filename)
        _write_jsonl(output_path, merged_text_records)

    return output_path


def _collect_files(directory: Path, pattern: str | None) -> list[Path]:
    """Collect files from *directory* according to matching rules.

    Args:
        directory: Directory to scan.
        pattern: Optional glob pattern.

    Returns:
        Sorted list of matched files.

    Raises:
        ValueError: If directory is invalid or no files match.
    """
    if not directory.exists():
        raise ValueError(f'Directory not found: {directory}')
    if not directory.is_dir():
        raise ValueError(f'Path is not a directory: {directory}')

    if pattern is None:
        files = sorted(
            (
                path
                for path in directory.iterdir()
                if path.is_file() and path.suffix.lower() in SUPPORTED_MERGE_EXTENSIONS
            ),
            key=lambda p: p.name,
        )
        if not files:
            raise ValueError(
                f'No supported files found in directory: {directory}. Supported extensions: .json, .yaml, .yml, .txt'
            )
        return files

    files = sorted(
        (path for path in directory.glob(pattern) if path.is_file()),
        key=lambda p: p.as_posix(),
    )
    if not files:
        raise ValueError(f'No files matched pattern "{pattern}" in directory: {directory}')
    return files


def _detect_format(files: list[Path]) -> MergeFormat:
    """Detect and validate a single normalized format across files.

    Args:
        files: Candidate files.

    Returns:
        Normalized merge format.

    Raises:
        ValueError: If any file has unsupported extension or formats are mixed.
    """
    formats: set[MergeFormat] = set()
    unsupported: list[Path] = []

    for file_path in files:
        fmt = _format_from_suffix(file_path.suffix)
        if fmt is None:
            unsupported.append(file_path)
            continue
        formats.add(fmt)

    if unsupported:
        names = ', '.join(path.name for path in unsupported)
        raise ValueError(f'Unsupported file format: {names}. Supported: .json, .yaml, .yml, .txt')

    if len(formats) != 1:
        found = ', '.join(sorted(fmt.value for fmt in formats))
        raise ValueError(f'All matched files must have the same format. Found: {found}')

    return next(iter(formats))


def _format_from_suffix(suffix: str) -> MergeFormat | None:
    """Map file suffix to normalized merge format.

    Args:
        suffix: File suffix including leading dot.

    Returns:
        Corresponding ``MergeFormat``, or ``None`` if unsupported.
    """
    lowered = suffix.lower()
    if lowered == '.json':
        return MergeFormat.JSON
    if lowered in {'.yaml', '.yml'}:
        return MergeFormat.YAML
    if lowered == '.txt':
        return MergeFormat.TEXT
    return None


def _merge_structured_files(files: list[Path], fmt: MergeFormat, with_filename: bool) -> list[dict[str, Any]]:
    """Load, validate schema consistency, and merge JSON/YAML files.

    Args:
        files: Structured files to merge.
        fmt: Structured format (JSON or YAML).
        with_filename: Whether to inject ``FILENAME``.

    Returns:
        Merged list of records.

    Raises:
        ValueError: If parsing or schema checks fail.
    """
    records_by_file: list[tuple[Path, list[dict[str, Any]]]] = []
    for file_path in files:
        records_by_file.append((file_path, _load_structured_records(file_path, fmt)))

    _validate_structured_schema(records_by_file)

    merged: list[dict[str, Any]] = []
    for file_path, records in records_by_file:
        if with_filename:
            merged.extend({**record, _FILENAME_FIELD: file_path.stem} for record in records)
        else:
            merged.extend(records)
    return merged


def _load_structured_records(file_path: Path, fmt: MergeFormat) -> list[dict[str, Any]]:
    """Load a structured file into normalized record list.

    Args:
        file_path: Source file path.
        fmt: File format.

    Returns:
        A list of object records.

    Raises:
        ValueError: If the file is invalid or has unsupported data shape.
    """
    try:
        content = file_path.read_text(encoding='utf-8')
    except OSError as exc:
        raise ValueError(f'Failed to read file {file_path}: {exc}') from exc

    if fmt == MergeFormat.JSON:
        data = _parse_json(content, file_path)
    elif fmt == MergeFormat.YAML:
        data = _parse_yaml(content, file_path)
    else:
        raise ValueError(f'Invalid structured format: {fmt}')

    if isinstance(data, dict):
        return [data]

    if isinstance(data, list):
        if not data:
            raise ValueError(f'File {file_path} must contain at least one object to validate schema.')
        if not all(isinstance(item, dict) for item in data):
            raise ValueError(f'File {file_path} must contain only objects when using list format.')
        return data

    raise ValueError(f'File {file_path} must contain an object or list of objects, not {type(data).__name__}.')


def _parse_json(content: str, file_path: Path) -> Any:
    """Parse JSON content with contextual error messages.

    Args:
        content: JSON text.
        file_path: Source file path.

    Returns:
        Parsed JSON value.

    Raises:
        ValueError: If JSON syntax is invalid.
    """
    try:
        return json.loads(content)
    except json.JSONDecodeError as exc:
        raise ValueError(f'Invalid JSON in {file_path}: {exc}') from exc


def _parse_yaml(content: str, file_path: Path) -> Any:
    """Parse YAML content with contextual error messages.

    Args:
        content: YAML text.
        file_path: Source file path.

    Returns:
        Parsed YAML value.

    Raises:
        ValueError: If YAML syntax is invalid.
    """
    try:
        return load_yaml(content)
    except YamlError as exc:
        raise ValueError(f'Invalid YAML in {file_path}: {exc}') from exc


def _validate_structured_schema(records_by_file: list[tuple[Path, list[dict[str, Any]]]]) -> None:
    """Validate that all structured records share the same inferred schema.

    Args:
        records_by_file: Structured records grouped by source file.

    Raises:
        ValueError: If any record schema differs from baseline schema.
    """
    baseline_schema: dict[str, Any] | None = None
    baseline_source = ''

    for file_path, records in records_by_file:
        for index, record in enumerate(records):
            schema = infer_schema(record)
            if baseline_schema is None:
                baseline_schema = schema
                baseline_source = f'{file_path.name}[{index}]'
                continue

            if schema != baseline_schema:
                raise ValueError(
                    f'Schema mismatch in {file_path.name}[{index}] compared with {baseline_source}. '
                    'All JSON/YAML records must share identical structure and types.'
                )


def _merge_text_files(files: list[Path], with_filename: bool) -> list[dict[str, Any]]:
    """Load text files and convert them to JSONL object records.

    Args:
        files: Text files to merge.
        with_filename: Whether to inject ``FILENAME``.

    Returns:
        JSON-serializable list of object records.

    Raises:
        ValueError: If reading any file fails.
    """
    merged: list[dict[str, Any]] = []
    for file_path in files:
        try:
            content = file_path.read_text(encoding='utf-8')
        except OSError as exc:
            raise ValueError(f'Failed to read file {file_path}: {exc}') from exc

        item: dict[str, Any] = {'text': content}
        if with_filename:
            item[_FILENAME_FIELD] = file_path.stem
        merged.append(item)
    return merged


def _resolve_output_path(directory: Path, fmt: MergeFormat) -> Path:
    """Resolve output path based on directory name and normalized format.

    Args:
        directory: Input directory.
        fmt: Merge format.

    Returns:
        Output file path.
    """
    extension = 'yaml' if fmt == MergeFormat.YAML else 'jsonl'
    return directory.parent / f'{directory.name}.{extension}'


def _write_jsonl(output_path: Path, records: list[dict[str, Any]]) -> None:
    """Write records as JSON Lines.

    Args:
        output_path: Output file path.
        records: Records to serialize.

    Raises:
        OSError: If writing fails.
    """
    lines = [json.dumps(record, ensure_ascii=False) for record in records]
    payload = '\n'.join(lines)
    if lines:
        payload += '\n'
    output_path.write_text(payload, encoding='utf-8')


def _write_yaml(output_path: Path, records: list[dict[str, Any]]) -> None:
    """Write records as YAML list.

    Args:
        output_path: Output file path.
        records: Records to serialize.

    Raises:
        OSError: If writing fails.
    """
    payload = dump_yaml(records, allow_unicode=True, default_flow_style=False, sort_keys=False)
    output_path.write_text(payload, encoding='utf-8')
