"""Field extraction and formatting for CLI output.

Extracts specific fields from data objects using dot notation paths,
with support for nested fields, arrays, and multiple output formats.

Usage:
    from cli.infrastructure.field_extractor import FieldExtractor, OutputFormat

    extractor = FieldExtractor(["id", "execution_metadata.total_tokens"])
    results = extractor.extract_many(content_parts)
    output = extractor.format(results, OutputFormat.TSV)
"""

import csv
import io
import json
from enum import Enum
from typing import Any, cast

from pydantic import BaseModel


class OutputFormat(str, Enum):
    """Supported output formats for field extraction."""

    TSV = "tsv"  # Tab-separated values (default for multi-field)
    CSV = "csv"  # Comma-separated with headers
    JSON = "json"  # JSON array
    JSONL = "jsonl"  # JSON Lines (one object per line)
    VALUES = "values"  # Raw values, newline-separated (single field only)


def _get_nested_value(obj: Any, path: str) -> Any:
    """Extract a value from an object using dot notation path.

    Supports:
    - Simple fields: "id", "name"
    - Nested fields: "execution_metadata.total_tokens"
    - Dict access: "content_structured.status"
    - Array field collection: "tags.tag_path" -> joins all tag_path values

    Args:
        obj: Object to extract from (Pydantic model, dict, or dataclass)
        path: Dot-notation path to the field

    Returns:
        The extracted value, or None if not found
    """
    parts = path.split(".")
    current: Any = obj

    for i, part in enumerate(parts):
        if current is None:
            return None

        # Handle Pydantic models
        if isinstance(current, BaseModel):
            if hasattr(current, part):
                current = getattr(current, part)
            else:
                return None

        # Handle dicts
        elif isinstance(current, dict):
            current_dict = cast(dict[str, Any], current)
            current = current_dict.get(part)

        # Handle lists - collect the field from all items
        elif isinstance(current, list):
            remaining_path = ".".join(parts[i:])
            values: list[Any] = []
            for item in cast(list[Any], current):
                val = _get_nested_value(item, remaining_path)
                if val is not None:
                    values.append(val)
            return ", ".join(str(v) for v in values) if values else None

        # Handle other objects with attributes
        else:
            current_obj = cast(object, current)
            if hasattr(current_obj, part):
                current = getattr(current_obj, part)
            else:
                return None

    return current


def _format_value(value: Any) -> str:
    """Format a value for display.

    Args:
        value: Value to format

    Returns:
        String representation
    """
    if value is None:
        return ""
    if isinstance(value, bool):
        return str(value).lower()
    if isinstance(value, (dict, list)):
        return json.dumps(value, ensure_ascii=False, default=str)
    return str(value)


class FieldExtractor:
    """Extracts and formats specific fields from data objects.

    Supports two modes:
    1. Basic field extraction: extract top-level or nested fields
    2. Sub-selection mode: extract specific keys from within JSON fields

    Sub-selection (jq_keys) works like jq's key selection. When provided,
    any dict/JSON field in the extracted results will be filtered to only
    include the specified keys.

    Example:
        # Extract id and content_structured, but only show 'profession' from content_structured
        extractor = FieldExtractor(["id", "content_structured"], jq_keys=["profession"])
    """

    def __init__(self, fields: list[str], jq_keys: list[str] | None = None):
        """Initialize with field paths to extract.

        Args:
            fields: List of dot-notation field paths (e.g., ["id", "execution_metadata.total_tokens"])
            jq_keys: Optional list of keys to extract from JSON/dict fields (jq-style sub-selection)
        """
        self.fields = fields
        self.jq_keys = jq_keys

    def extract_one(self, obj: Any) -> dict[str, Any]:
        """Extract fields from a single object.

        Args:
            obj: Object to extract from

        Returns:
            Dict mapping field paths to extracted values
        """
        result = {field: _get_nested_value(obj, field) for field in self.fields}

        # Apply jq-style sub-selection if specified
        if self.jq_keys:
            result = self._apply_jq_selection(result)

        return result

    def _apply_jq_selection(self, result: dict[str, Any]) -> dict[str, Any]:
        """Apply jq-style key selection to dict/JSON fields.

        For each dict value in the result, filter to only include the specified keys.
        Supports nested key paths (e.g., "user.email" to get {"user": {"email": ...}}).

        Args:
            result: Extracted field values

        Returns:
            Result with dict fields filtered to only include jq_keys
        """
        filtered: dict[str, Any] = {}
        for field, value in result.items():
            if isinstance(value, dict) and self.jq_keys:
                filtered[field] = self._select_keys(
                    cast(dict[str, Any], value), self.jq_keys
                )
            else:
                filtered[field] = value
        return filtered

    def _select_keys(self, data: dict[str, Any], keys: list[str]) -> dict[str, Any]:
        """Select specific keys from a dict, supporting nested paths.

        Args:
            data: Source dict
            keys: Keys to select (can include nested paths like "user.email")

        Returns:
            Dict containing only the selected keys
        """
        result: dict[str, Any] = {}

        for key in keys:
            if "." in key:
                # Nested path - drill down
                parts = key.split(".", 1)
                root, rest = parts[0], parts[1]
                if root in data and isinstance(data[root], dict):
                    nested_result = self._select_keys(data[root], [rest])
                    if nested_result:
                        if root not in result:
                            result[root] = {}
                        result[root].update(nested_result)
            elif key in data:
                result[key] = data[key]

        return result

    def extract_many(self, objects: list[Any]) -> list[dict[str, Any]]:
        """Extract fields from multiple objects.

        Args:
            objects: List of objects to extract from

        Returns:
            List of dicts mapping field paths to extracted values
        """
        return [self.extract_one(obj) for obj in objects]

    def format(
        self,
        results: list[dict[str, Any]],
        output_format: OutputFormat = OutputFormat.TSV,
    ) -> str:
        """Format extracted results in the specified format.

        Args:
            results: List of extracted field dicts
            output_format: Output format to use

        Returns:
            Formatted string output
        """
        if output_format == OutputFormat.JSON:
            return self._format_json(results)
        elif output_format == OutputFormat.JSONL:
            return self._format_jsonl(results)
        elif output_format == OutputFormat.CSV:
            return self._format_csv(results)
        elif output_format == OutputFormat.TSV:
            return self._format_tsv(results)
        elif output_format == OutputFormat.VALUES:
            return self._format_values(results)
        else:
            return self._format_tsv(results)

    def _format_json(self, results: list[dict[str, Any]]) -> str:
        """Format as JSON array."""
        return json.dumps(results, indent=2, ensure_ascii=False, default=str)

    def _format_jsonl(self, results: list[dict[str, Any]]) -> str:
        """Format as JSON Lines (one object per line)."""
        lines = [json.dumps(r, ensure_ascii=False, default=str) for r in results]
        return "\n".join(lines)

    def _format_csv(self, results: list[dict[str, Any]]) -> str:
        """Format as CSV with headers."""
        if not results:
            return ""

        output = io.StringIO()
        writer = csv.writer(output)

        # Write header
        writer.writerow(self.fields)

        # Write rows
        for result in results:
            row = [_format_value(result.get(field)) for field in self.fields]
            writer.writerow(row)

        return output.getvalue().rstrip("\n")

    def _format_tsv(self, results: list[dict[str, Any]]) -> str:
        """Format as tab-separated values (no header)."""
        lines: list[str] = []
        for result in results:
            values = [_format_value(result.get(field)) for field in self.fields]
            lines.append("\t".join(values))
        return "\n".join(lines)

    def _format_values(self, results: list[dict[str, Any]]) -> str:
        """Format as raw values, one per line.

        Best for single-field extraction for piping.
        """
        lines: list[str] = []
        for result in results:
            # For single field, just output the value
            if len(self.fields) == 1:
                lines.append(_format_value(result.get(self.fields[0])))
            else:
                # For multiple fields, space-separate on each line
                values = [_format_value(result.get(field)) for field in self.fields]
                lines.append(" ".join(values))
        return "\n".join(lines)


def parse_fields_option(fields_str: str) -> list[str]:
    """Parse the --fields option value into a list of field paths.

    Args:
        fields_str: Comma-separated field paths (e.g., "id,content_text,execution_metadata.total_tokens")

    Returns:
        List of field paths
    """
    return [f.strip() for f in fields_str.split(",") if f.strip()]


def parse_jq_keys_option(jq_str: str) -> list[str]:
    """Parse the --jq option value into a list of keys.

    Args:
        jq_str: Comma-separated keys to select from JSON fields (e.g., "profession,name,skills.primary")

    Returns:
        List of key paths
    """
    return [k.strip() for k in jq_str.split(",") if k.strip()]


# Available fields documentation for help text
AVAILABLE_FIELDS = """
Available fields for --fields/-F option:

Top-level fields:
  id                           Content part UUID
  message_id                   Parent message UUID
  message_role                 Role: user or assistant
  part_index                   Index within message (0-based)
  content_type_id              Content type (text, code, json, etc.)
  content_text                 Plain text content
  content_structured           Full structured JSON content
  content_structured.<key>     Access nested keys in structured content
  created_at                   Creation timestamp

Session fields:
  session.id                   Chat session UUID
  session.name                 Session name
  session.last_activity_at     Last activity timestamp

Tag fields:
  tags.tag_path                All tag paths (comma-separated)
  tags.display_name            All tag display names

Execution metadata fields:
  execution_metadata.agent_execution_id    Execution UUID
  execution_metadata.agent_id              Agent UUID
  execution_metadata.agent_name            Agent name
  execution_metadata.provider_key          Provider (anthropic, openai, etc.)
  execution_metadata.provider_model_name   Model name
  execution_metadata.input_tokens          Input token count
  execution_metadata.output_tokens         Output token count
  execution_metadata.total_tokens          Total token count
  execution_metadata.total_cost_usd        Total cost in USD
  execution_metadata.total_duration_ms     Execution duration in ms
  execution_metadata.status_id             Execution status

Examples:
  -F id,content_text                              # ID and text
  -F execution_metadata.total_tokens              # Just token count
  -F id,content_structured.status                 # ID and nested field
  -F tags.tag_path                                # All tag paths

Sub-selection with --jq:
  -F id,content_structured --jq profession        # Show only 'profession' from structured
  -F content_structured --jq name,skills          # Multiple keys
  -F content_structured --jq user.email           # Nested key path
"""
