"""Centralized sinks and event pipeline for task definitions.

This module wires parser events to JSONL/CSV writers and to the live
metrics display using MetricsManager. Tasks do not construct writers
directly; they only provide a parser and optional metrics config.
"""

from __future__ import annotations

import os
from dataclasses import dataclass
from typing import Any, Callable, Optional

# DisplayBackend imported via TYPE_CHECKING to avoid circular dependency
from hil_testbench.data_processing.writers import CsvWriter, JsonlWriter
from hil_testbench.run.task_logger import TaskLogger, LogLevel
from hil_testbench.data_structs.parameters import ParametersSchema
from hil_testbench.data_structs.parameters_utils import validate_event_schema
import time
import copy


@dataclass
class EventSinks:
    """Holds the event sinks used in the pipeline callback."""

    jsonl: JsonlWriter | None
    csv: CsvWriter | None


def _make_sinks(
    exec_dir: str,
    file_base: str,
    enable_jsonl: bool,
    enable_csv: bool,
    schema: ParametersSchema | None = None,
) -> EventSinks:
    jsonl_writer: JsonlWriter | None = None
    csv_writer: CsvWriter | None = None

    if enable_jsonl:
        jsonl_path = os.path.join(exec_dir, f"{file_base}.jsonl")
        jsonl_writer = JsonlWriter(jsonl_path, schema=schema)

    if enable_csv:
        # CSV fieldnames are determined lazily from first event
        # We create the writer on first write when we know the keys
        csv_writer = None  # type: ignore[assignment]

    return EventSinks(jsonl_writer, csv_writer)  # type: ignore[arg-type]


def _deep_merge_display_config(
    schema: ParametersSchema | None,
    yaml_config: dict | None,
    logger: TaskLogger | None = None,
) -> ParametersSchema | None:
    """Deep merge schema ParameterField with YAML display config.

    Args:
        schema: Base ParametersSchema from task definition
        yaml_config: YAML tasks.<task>.display.parameters config
        logger: TaskLogger for validation warnings

    Returns:
        Merged ParametersSchema with validation applied

    Supports wildcard pattern matching:
        - Exact name match takes precedence
        - Falls back to fnmatch pattern (e.g., "bits_per_second_*")
    """
    if not schema or not yaml_config:
        return schema

    import fnmatch  # hil: allow-lazy - only needed for wildcard pattern matching

    # Create deep copy to avoid mutating original
    merged = copy.deepcopy(schema)

    for field in merged.fields:
        # Try exact match first
        param_config = yaml_config.get(field.name)

        # Fall back to wildcard pattern matching if no exact match
        if not param_config:
            for pattern, config in yaml_config.items():
                if fnmatch.fnmatch(field.name, pattern):
                    param_config = config
                    break

        if not param_config:
            continue

        # Merge individual fields with validation
        try:
            # Simple field overrides
            if "display_enabled" in param_config:
                field.display_enabled = bool(param_config["display_enabled"])
            if "trendline_length" in param_config:
                field.trendline_length = int(param_config["trendline_length"])
            if "trendline_window_sec" in param_config:
                field.trendline_window_sec = float(param_config["trendline_window_sec"])
            if "display_order" in param_config:
                field.display_order = int(param_config["display_order"])
            if "display_group" in param_config:
                field.display_group = str(param_config["display_group"])
            if "precision" in param_config:
                field.precision = int(param_config["precision"])
            if "scale_strategy" in param_config:
                field.scale_strategy = str(param_config["scale_strategy"])
            if "normalize_range" in param_config:
                nr = param_config["normalize_range"]
                if isinstance(nr, (list, tuple)) and len(nr) == 2:
                    field.normalize_range = (float(nr[0]), float(nr[1]))

            # Deep merge thresholds
            if "thresholds" in param_config:
                yaml_thresholds = param_config["thresholds"]
                if isinstance(yaml_thresholds, dict):
                    # Merge individual threshold levels
                    for level, threshold_data in yaml_thresholds.items():
                        if isinstance(threshold_data, dict):
                            # Merge with existing or create new
                            if level not in field.thresholds:
                                from hil_testbench.data_structs.parameters import (  # hil: allow-lazy - avoid circular import
                                    Threshold,
                                )

                                val = threshold_data.get("value")
                                if isinstance(val, (list, tuple)) and len(val) == 2:
                                    value_typed: float | tuple[float, float] = (
                                        float(val[0]),
                                        float(val[1]),
                                    )
                                elif isinstance(val, (int, float)):
                                    value_typed = float(val)
                                else:
                                    value_typed = 0.0
                                field.thresholds[level] = Threshold(
                                    value=value_typed,
                                    mode=threshold_data.get("operator", "range"),
                                    color=threshold_data.get("color", ""),
                                    description=threshold_data.get("description", ""),
                                )
                            else:
                                # Update existing threshold
                                if "value" in threshold_data:
                                    v = threshold_data["value"]
                                    if isinstance(v, (list, tuple)) and len(v) == 2:
                                        field.thresholds[level].value = (
                                            float(v[0]),
                                            float(v[1]),
                                        )
                                    elif isinstance(v, (int, float)):
                                        field.thresholds[level].value = float(v)
                                if "operator" in threshold_data:
                                    field.thresholds[level].mode = threshold_data[
                                        "operator"
                                    ]
                                if "color" in threshold_data:
                                    field.thresholds[level].color = threshold_data[
                                        "color"
                                    ]

        except (ValueError, TypeError) as e:
            # Validation failed - log warning and use schema defaults
            if logger:
                logger.console_print(
                    f"[Display Config Validation] Warning: Invalid YAML override for parameter '{field.name}': {e}. Using schema defaults.",
                    "WARNING",
                )
                logger.log_warning(
                    "display_config_validation_failed",
                    parameter=field.name,
                    error=str(e),
                    message="Using schema defaults for this parameter",
                )

    return merged


def _ensure_csv_writer(
    sinks: EventSinks,
    exec_dir: str,
    file_base: str,
    event: dict[str, Any],
    schema: ParametersSchema | None = None,
) -> None:
    if sinks.csv is not None:
        return
    fieldnames = list(event.keys())
    csv_path = os.path.join(exec_dir, f"{file_base}.csv")
    sinks.csv = CsvWriter(csv_path, fieldnames, schema=schema)


def make_pipeline_callback_factory(
    *,
    task_name: str,
    command_name: str,
    parser_factory: Callable[[], Any] | None,
    display_backend: Any | None,
    enable_jsonl: bool,
    enable_csv: bool,
    custom_file_base: str | None = None,
    task_logger: TaskLogger | None = None,
    parameters_schema: ParametersSchema | None = None,
    display_config: dict | None = None,
) -> Callable[[str], Callable[[str, bool], None]]:
    """Create a callback factory wiring parser -> sinks -> display backend.

    Returns a factory suitable for TaskRunner's callback factory detection:
    factory(exec_dir) -> callback(line, is_error).
    """

    def factory(exec_dir: str) -> Callable[[str, bool], None]:
        file_base = custom_file_base or f"{task_name}_{command_name}"
        sinks = _make_sinks(
            exec_dir, file_base, enable_jsonl, enable_csv, schema=parameters_schema
        )

        # Deep merge schema with YAML display config
        merged_schema = _deep_merge_display_config(
            parameters_schema, display_config, task_logger
        )

        # Pass schema to display backend
        if display_backend and merged_schema and hasattr(display_backend, "set_schema"):
            try:
                display_backend.set_schema(task_name, merged_schema)
            except Exception as e:  # pylint: disable=broad-except
                if task_logger:
                    task_logger.log_warning(
                        "display_set_schema_failed",
                        task=task_name,
                        error=str(e),
                        error_type=type(e).__name__,
                    )

        parser = parser_factory() if parser_factory else None

        # Structured pipeline config logging via provided TaskLogger (singleton)
        if task_logger:
            parser_type = type(parser).__name__ if parser else None
            # Command-scoped pipeline configuration (appears in per-command log)
            task_logger.log_command(
                "pipeline_configured",
                command_name,
                LogLevel.INFO,
                task_name=task_name,
                parser=parser_type,
                jsonl_path=sinks.jsonl.path if sinks.jsonl else None,
                csv_path=getattr(sinks.csv, "path", None) if sinks.csv else None,
                metrics_enabled=bool(display_backend),
            )

        def callback(line: str, is_error: bool) -> None:
            # If no parser, we only mirror raw lines to sinks (as text events)
            if parser is None:
                if is_error:
                    return
                event = {"message": line}
                if sinks.jsonl:
                    try:
                        sinks.jsonl.write(event)
                    except Exception as e:  # pylint: disable=broad-except
                        if task_logger:
                            task_logger.log_error(
                                "jsonl_write_failed",
                                task=task_name,
                                error=str(e),
                                error_type=type(e).__name__,
                            )
                if enable_csv:
                    try:
                        _ensure_csv_writer(
                            sinks, exec_dir, file_base, event, schema=parameters_schema
                        )
                        assert sinks.csv is not None
                        sinks.csv.write(event)
                    except Exception as e:  # pylint: disable=broad-except
                        if task_logger:
                            task_logger.log_warning(
                                "csv_write_failed",
                                task=task_name,
                                error=str(e),
                                error_type=type(e).__name__,
                            )
                if display_backend:
                    try:
                        # Use merged schema to check if display enabled
                        timestamp = time.time()
                        display_backend.update_parameter(
                            task_name, event.get("message", "raw"), event, timestamp
                        )
                    except Exception as e:  # pylint: disable=broad-except
                        if task_logger:
                            task_logger.log_error(
                                "display_update_failed",
                                task=task_name,
                                error=str(e),
                                error_type=type(e).__name__,
                            )
                return

            try:
                # Pass context to parser (command_name, task_name)
                context = {"command_name": command_name, "task_name": task_name}
                events = parser.feed(line, is_error, context=context)
            except Exception as e:  # pylint: disable=broad-except
                if task_logger:
                    task_logger.log_error(
                        "parser_feed_error",
                        task=task_name,
                        error=str(e),
                        error_type=type(e).__name__,
                        line_preview=line[:100] if len(line) > 100 else line,
                    )
                    # Report error to display backend
                    backend = getattr(task_logger, "_display_backend", None)
                    if backend and hasattr(backend, "report_task_error"):
                        try:
                            backend.report_task_error(
                                task_name, f"Parser error: {type(e).__name__}: {str(e)}"
                            )
                        except Exception:  # pylint: disable=broad-except
                            pass
                return

            if not events:
                return

            for ev in events:
                # Write to sinks
                if sinks.jsonl:
                    try:
                        sinks.jsonl.write(ev)
                    except Exception as e:  # pylint: disable=broad-except
                        if task_logger:
                            task_logger.log_error(
                                "jsonl_write_failed",
                                task=task_name,
                                error=str(e),
                                error_type=type(e).__name__,
                            )
                if enable_csv:
                    try:
                        _ensure_csv_writer(
                            sinks, exec_dir, file_base, ev, schema=parameters_schema
                        )
                        assert sinks.csv is not None
                        sinks.csv.write(ev)
                    except Exception as e:  # pylint: disable=broad-except
                        # CSV writing can fail due to schema mismatches (e.g., auto-added
                        # timestamp field). Don't let CSV errors break metrics display.
                        if task_logger:
                            task_logger.log_warning(
                                "csv_write_failed",
                                task=task_name,
                                error=str(e),
                                error_type=type(e).__name__,
                            )

                # Update display backend
                if display_backend and merged_schema:
                    try:
                        # Support multi-metric events by using a parameter name when present
                        param_name = (
                            ev.get("param_name")
                            or ev.get("sensor_name")
                            or ev.get("name")
                        )

                        # Check if parameter is enabled in schema
                        if isinstance(param_name, str) and param_name:
                            # Find field in schema
                            field = next(
                                (
                                    f
                                    for f in merged_schema.fields
                                    if f.name == param_name
                                ),
                                None,
                            )
                            if field and field.display_enabled:
                                # Enrich event with schema metadata (unit, thresholds, formatting)
                                enriched_ev = ev.copy()
                                enriched_ev["unit"] = field.unit
                                enriched_ev["precision"] = field.precision
                                enriched_ev["scale"] = field.scale_strategy
                                enriched_ev["description"] = (
                                    field.description or param_name
                                )
                                if field.thresholds:
                                    # Convert Threshold dataclass objects to dicts
                                    enriched_ev["thresholds"] = {
                                        level: {
                                            "value": threshold.value,
                                            "operator": threshold.mode,
                                            "color": threshold.color,
                                        }
                                        for level, threshold in field.thresholds.items()
                                    }

                                timestamp = time.time()
                                display_backend.update_parameter(
                                    task_name, param_name, enriched_ev, timestamp
                                )
                        else:
                            # No param name - try to find parameter from event keys
                            # This handles iperf case where param_name is not in event,
                            # but bits_per_second is a direct key
                            for field in merged_schema.fields:
                                if field.name in ev and field.display_enabled:
                                    # Enrich event with schema metadata
                                    enriched_ev = ev.copy()
                                    enriched_ev["unit"] = field.unit
                                    enriched_ev["precision"] = field.precision
                                    enriched_ev["description"] = (
                                        field.description or field.name
                                    )
                                    enriched_ev["scale"] = field.scale_strategy
                                    if field.thresholds:
                                        # Convert Threshold dataclass objects to dicts
                                        enriched_ev["thresholds"] = {
                                            level: {
                                                "value": threshold.value,
                                                "operator": threshold.mode,
                                                "color": threshold.color,
                                            }
                                            for level, threshold in field.thresholds.items()
                                        }

                                    timestamp = time.time()
                                    display_backend.update_parameter(
                                        task_name, field.name, enriched_ev, timestamp
                                    )
                                    break  # Only update first matching field
                    except Exception as e:  # pylint: disable=broad-except
                        if task_logger:
                            task_logger.log_error(
                                "display_update_failed",
                                task=task_name,
                                error=str(e),
                                error_type=type(e).__name__,
                            )

        return callback

    return factory


class BaseSink:
    """Base sink supporting schema validation for output fields before writing."""

    def __init__(self, schema: Optional[ParametersSchema] = None):
        self.schema = schema

    def validate(self, event: dict):
        """Validate event dict against schema fields."""
        if self.schema:
            validate_event_schema(event, self.schema, strict=True)
        return True

    def write(self, event: dict):
        """Validate and write event. Subclasses should implement actual writing logic."""
        self.validate(event)
        self._write_event(event)

    def _write_event(self, event: dict):
        """Actual writing logic to be implemented by subclasses."""
        raise NotImplementedError
