from __future__ import annotations

from collections.abc import Callable, Mapping, Sequence
import logging
from pathlib import Path

from .csv_files import load_csv_file_records
from .json_files import load_json_file_records
from ..utils.record_tools import (
    DEFAULT_KEEP_KEYS,
    EXTRA_FIELDS_KEY,
    EXTRA_TAGS_KEY,
    split_record,
)
from ..write_data.write_batch import write_batch
from ..write_data.writer import QOI_FIELD_KEYS

logger = logging.getLogger(__name__)

SUPPORTED_FILE_EXTENSIONS = frozenset({".json", ".csv"})


def ingest_data(
    folder: str | Path,
    *,
    file_name: str,
    csv_delimiter: str = ",",
    continue_on_error: bool = True,
) -> list[dict[str, object]]:
    """Recursively load raw records from files in a folder.

    Parameters
    ----------
    folder : str | Path
        Folder to scan recursively.
    file_name : str
        Target file name or glob pattern ending with ``.json`` or ``.csv``.
        Files matching this value are loaded recursively.
    csv_delimiter : str, default=","
        Delimiter used for CSV parsing.
    continue_on_error : bool, default=True
        If ``True``, skip unreadable files and continue.

    Returns
    -------
    list[dict[str, object]]
        Raw loaded records.

    Raises
    ------
    FileNotFoundError
        If folder does not exist.
    ValueError
        If ``file_name`` extension is unsupported.
    """
    root_path = Path(folder).expanduser()
    if not root_path.exists():
        raise FileNotFoundError(f"Folder not found: {root_path}")

    loader = _select_loader(file_name=file_name, csv_delimiter=csv_delimiter)
    paths = sorted(path for path in root_path.rglob(file_name) if path.is_file())

    records: list[dict[str, object]] = []
    for file_path in paths:
        try:
            records.extend(loader(file_path))
        except Exception:
            logger.exception("Failed parsing file '%s'.", file_path)
            if not continue_on_error:
                raise
    return records


def process_data(
    raw_data: Sequence[Mapping[str, object]],
    *,
    keep_keys: Sequence[str] | None = None,
    continue_on_error: bool = False,
) -> list[dict[str, object]]:
    """Normalize and validate raw records before writing.

    Parameters
    ----------
    raw_data : Sequence[Mapping[str, object]]
        Raw records from ``ingest_data``.
    keep_keys : Sequence[str] | None, default=None
        Keys to keep from each record. If ``None``, canonical keys are used.
    continue_on_error : bool, default=False
        If ``True``, skip invalid records and continue.

    Returns
    -------
    list[dict[str, object]]
        Processed records ready for ``write_batch``.

    Raises
    ------
    ValueError
        If a record is invalid and ``continue_on_error`` is ``False``.
    """
    keep = set(keep_keys) if keep_keys is not None else set(DEFAULT_KEEP_KEYS)
    keep.update({"qoi", EXTRA_TAGS_KEY, EXTRA_FIELDS_KEY})

    processed: list[dict[str, object]] = []
    for raw_record in raw_data:
        try:
            filtered = {key: value for key, value in raw_record.items() if key in keep}
            record, extra_tags, extra_fields = split_record(filtered)
            _validate_processed_record(record, extra_fields)

            normalized: dict[str, object] = dict(record)
            if extra_tags:
                normalized[EXTRA_TAGS_KEY] = dict(extra_tags)
            if extra_fields:
                normalized[EXTRA_FIELDS_KEY] = dict(extra_fields)
            processed.append(normalized)
        except Exception:
            logger.exception("Failed processing one raw record.")
            if not continue_on_error:
                raise

    return processed


def ingest_and_write(
    folder: str | Path,
    *,
    file_name: str,
    csv_delimiter: str = ",",
    keep_keys: Sequence[str] | None = None,
    settings_path: str | Path | None = None,
    continue_on_error: bool = True,
) -> dict[str, int]:
    """Ingest, process, and write records in one call.

    Parameters
    ----------
    folder : str | Path
        Folder to scan recursively.
    file_name : str
        Target file name or glob pattern ending with ``.json`` or ``.csv``.
    csv_delimiter : str, default=","
        Delimiter used for CSV parsing.
    keep_keys : Sequence[str] | None, default=None
        Keys to keep before writing.
    settings_path : str | Path | None, default=None
        Optional settings TOML path.
    continue_on_error : bool, default=True
        If ``True``, continue after per-file, per-record, and write failures.

    Returns
    -------
    dict[str, int]
        Write summary plus ``raw_records_total`` and ``processed_records_total``.
    """
    raw_data = ingest_data(
        folder,
        file_name=file_name,
        csv_delimiter=csv_delimiter,
        continue_on_error=continue_on_error,
    )
    processed_data = process_data(
        raw_data,
        keep_keys=keep_keys,
        continue_on_error=continue_on_error,
    )
    write_summary = write_batch(
        processed_data,
        settings_path=settings_path,
        continue_on_error=continue_on_error,
    )

    return {
        "raw_records_total": len(raw_data),
        "processed_records_total": len(processed_data),
        **write_summary,
    }


def _select_loader(
    *,
    file_name: str,
    csv_delimiter: str,
) -> Callable[[Path], list[dict[str, object]]]:
    """Select file loader from file name extension.

    Parameters
    ----------
    file_name : str
        Target file name or pattern.
    csv_delimiter : str
        Delimiter for CSV parsing.

    Returns
    -------
    Callable[[Path], list[dict[str, object]]]
        Loader function for files with the detected extension.

    Raises
    ------
    ValueError
        If extension is unsupported.
    """
    extension = Path(file_name).suffix.lower()
    if extension not in SUPPORTED_FILE_EXTENSIONS:
        supported = ", ".join(sorted(SUPPORTED_FILE_EXTENSIONS))
        raise ValueError(
            f"Unsupported file extension '{extension or '<none>'}'. "
            f"Supported: {supported}."
        )

    if extension == ".json":
        return load_json_file_records
    return lambda path: load_csv_file_records(path, delimiter=csv_delimiter)


def _validate_processed_record(
    record: Mapping[str, object],
    extra_fields: Mapping[str, object] | None,
) -> None:
    """Validate required pieces of a processed record."""
    qoi = record.get("qoi")
    if not isinstance(qoi, str) or not qoi.strip():
        raise ValueError("Each processed record must include a non-empty 'qoi'.")

    has_builtin_field = any(record.get(key) is not None for key in QOI_FIELD_KEYS)
    has_extra_field = bool(extra_fields)
    if not (has_builtin_field or has_extra_field):
        raise ValueError(
            "Each processed record must include at least one field value "
            "or provide 'extra_fields'."
        )
