"""Input resolution module for dotprompt.

Handles file loading, type detection, path security, and schema inference
for all input types (None, dict, list[dict], str→files, list[str]→files).
"""

import json
from pathlib import Path
from typing import Any

import yaml


def resolve_input(
    raw_input: Any,
    prompt_file_path: Path,
    defaults: dict[str, Any] | None = None,
) -> list[dict[str, Any]]:
    """Resolve raw input to a list of dicts.

    When *defaults* is provided, each resolved record is merged with
    ``{**defaults, **record}`` so that per-record values override defaults.

    Args:
        raw_input: Input to resolve (None, dict, list[dict], str, list[str]).
        prompt_file_path: Path to .prompt file (for resolving relative paths).
        defaults: Optional fixed values merged into every record.

    Returns:
        list[dict]: Normalized input data as list of dicts.

    Raises:
        ValueError: On unsupported types, invalid file paths, or file format errors.
    """
    records = _resolve_raw_input(raw_input, prompt_file_path)
    if defaults and records:
        records = [{**defaults, **record} for record in records]
    return records


def _resolve_raw_input(raw_input: Any, prompt_file_path: Path) -> list[dict[str, Any]]:
    """Core resolution logic without defaults merging.

    Args:
        raw_input: Input to resolve (None, dict, list[dict], str, list[str]).
        prompt_file_path: Path to .prompt file (for resolving relative paths).

    Returns:
        list[dict]: Normalized input data as list of dicts.

    Raises:
        ValueError: On unsupported types, invalid file paths, or file format errors.
    """
    prompt_dir = prompt_file_path.parent

    # None → empty list
    if raw_input is None:
        return []

    # dict → single-item list
    if isinstance(raw_input, dict):
        return [raw_input]

    # list → detect if list[dict] or list[str]
    if isinstance(raw_input, list):
        if not raw_input:
            return []

        # Check first element to determine type
        first = raw_input[0]

        # list[dict]
        if isinstance(first, dict):
            # Validate all items are dicts
            if not all(isinstance(item, dict) for item in raw_input):
                raise ValueError('List must contain only dicts or only strings')
            return raw_input

        # list[str] → load all files, concatenate
        if isinstance(first, str):
            # Validate all items are strings
            if not all(isinstance(item, str) for item in raw_input):
                raise ValueError('List must contain only dicts or only strings')

            # Load all files and concatenate
            all_items = []
            for file_ref in raw_input:
                file_path = _validate_file_path(file_ref, prompt_dir)
                data = _load_file(file_path)

                # Normalize to list
                if isinstance(data, dict):
                    all_items.append(data)
                elif isinstance(data, list):
                    all_items.extend(data)
                else:
                    raise ValueError(f'File {file_ref} must contain dict or list[dict], not {type(data).__name__}')

            return all_items

        # Invalid list type
        raise ValueError(f'List must contain dicts or strings, not {type(first).__name__}')

    # str → load file
    if isinstance(raw_input, str):
        file_path = _validate_file_path(raw_input, prompt_dir)
        data = _load_file(file_path)

        # JSONL always returns list
        if file_path.suffix.lower() == '.jsonl':
            return data if isinstance(data, list) else [data]

        # JSON/YAML: dict → single-item list, list → list
        if isinstance(data, dict):
            return [data]
        elif isinstance(data, list):
            return data
        else:
            raise ValueError(f'File {raw_input} must contain dict or list[dict], not {type(data).__name__}')

    # Invalid types (bool, int, float, etc.)
    raise ValueError(
        f'Unsupported input type: {type(raw_input).__name__}. '
        f'Expected None, dict, list[dict], str (file path), or list[str] (file paths)'
    )


def _validate_file_path(file_ref: str, prompt_dir: Path) -> Path:
    """Resolve and validate a file path.

    Args:
        file_ref: File path string (relative or absolute).
        prompt_dir: Directory containing .prompt file (base for relative paths).

    Returns:
        Resolved absolute Path object.

    Raises:
        ValueError: If file does not exist.
    """
    # Resolve relative to prompt_dir
    if Path(file_ref).is_absolute():
        resolved = Path(file_ref).resolve()
    else:
        resolved = (prompt_dir / file_ref).resolve()

    # Check file exists
    if not resolved.exists():
        raise ValueError(f'File not found: {file_ref}')

    return resolved


def _load_file(path: Path) -> dict | list[dict]:
    """Load file content based on extension.

    Args:
        path: Absolute path to file.

    Returns:
        dict (single mode) or list[dict] (batch mode).

    Raises:
        ValueError: On unsupported extension, read errors, or invalid format.
    """
    ext = path.suffix.lower()

    # Read file content
    try:
        content = path.read_text(encoding='utf-8')
    except OSError as exc:
        raise ValueError(f'Failed to read file {path}: {exc}') from exc

    # JSON format
    if ext == '.json':
        try:
            data = json.loads(content)
        except json.JSONDecodeError as exc:
            raise ValueError(f'Invalid JSON in {path}: {exc}') from exc

        # Validate: must be dict or list[dict]
        if isinstance(data, dict):
            return data
        elif isinstance(data, list):
            if not all(isinstance(item, dict) for item in data):
                raise ValueError(f'JSON array in {path} must contain only objects (dicts)')
            return data
        else:
            raise ValueError(f'JSON in {path} must be object or array, not {type(data).__name__}')

    # YAML format
    elif ext in ('.yaml', '.yml'):
        if yaml is None:
            raise ValueError('YAML support requires PyYAML: pip install pyyaml')
        try:
            data = yaml.safe_load(content)
        except yaml.YAMLError as exc:
            raise ValueError(f'Invalid YAML in {path}: {exc}') from exc

        # Validate: must be dict or list[dict]
        if isinstance(data, dict):
            return data
        elif isinstance(data, list):
            if not all(isinstance(item, dict) for item in data):
                raise ValueError(f'YAML array in {path} must contain only objects (dicts)')
            return data
        else:
            raise ValueError(f'YAML in {path} must be object or array, not {type(data).__name__}')

    # JSONL format (always returns list)
    elif ext == '.jsonl':
        items = []
        for line_num, line in enumerate(content.splitlines(), start=1):
            stripped = line.strip()
            if not stripped:  # Skip blank lines
                continue
            try:
                obj = json.loads(stripped)
            except json.JSONDecodeError as exc:
                raise ValueError(f'Invalid JSON at line {line_num} in {path}: {exc}') from exc
            if not isinstance(obj, dict):
                raise ValueError(f'JSONL line {line_num} in {path} must be object, not {type(obj).__name__}')
            items.append(obj)
        return items

    else:
        raise ValueError(f'Unsupported file format: {ext}. Supported: .json, .yaml, .yml, .jsonl')


def infer_schema(data: dict[str, Any]) -> dict:
    """Infer JSON Schema from a dict.

    Args:
        data: Dictionary to infer schema from.

    Returns:
        JSON Schema dict with type "object" and properties.
    """
    properties = {}

    for key, value in data.items():
        properties[key] = _infer_type(value)

    return {'type': 'object', 'properties': properties}


def _infer_type(value: Any) -> dict:
    """Infer JSON Schema type for a value.

    Args:
        value: Python value to infer type from.

    Returns:
        JSON Schema type dict (e.g. {"type": "string"}).
    """
    # None → string (nullable)
    if value is None:
        return {'type': 'string'}

    # bool BEFORE int (bool is subclass of int in Python)
    if isinstance(value, bool):
        return {'type': 'boolean'}

    # int
    if isinstance(value, int):
        return {'type': 'integer'}

    # float
    if isinstance(value, float):
        return {'type': 'number'}

    # str
    if isinstance(value, str):
        return {'type': 'string'}

    # list → array
    if isinstance(value, list):
        if not value:
            # Empty list → array with no items constraint
            return {'type': 'array'}
        # Infer from first element
        return {'type': 'array', 'items': _infer_type(value[0])}

    # dict → nested object
    if isinstance(value, dict):
        nested_props = {}
        for k, v in value.items():
            nested_props[k] = _infer_type(v)
        return {'type': 'object', 'properties': nested_props}

    # Fallback for unknown types
    return {'type': 'string'}
