"""Main tool for QualPipe backend that coordinates the reading of the metrics and application of criteria."""


from astropy.table import join as astropy_join
from ctapipe.core import Tool
from ctapipe.core.traits import Bool, List, Path
from ctapipe.core.traits import Enum as TraitEnum
from ctapipe.instrument import SubarrayDescription
from ctapipe.io import HDF5MonitoringSource, read_table, write_table
from ctapipe.io.hdf5dataformat import (
    DL1_SUBARRAY_QUALITY_GROUP,
    DL1_SUBARRAY_TRIGGER_TABLE,
    DL1_TEL_QUALITY_GROUP,
)
from ctapipe.io.monitoringtypes import MonitoringType
from traitlets.config import Config

from ..core.metric import DataCategory, Metric


class MetricEvaluator(Tool):
    """
    Main class to run the automatic QualPipe.

    The MetricEvaluator class is responsible for managing the sequence of operations required to
    prepare metrics and criteria according to the configuration files. It coordinates the retrieval,
    computation, and application of metrics and criteria within QualPipe.


    :Example:

    Configuration example:

    .. code-block:: yaml

        MetricEvaluator:
          input_url: "dummy_data_file.h5"
          data_category: DL1_EVENT
          metrics:
            - name: my_array_measurement                # this name will be used in report to identify the measurement
              input_source: "/my_table/my_data_array"   # path to the data in the HDF5 file
              array_element: LST
              telescope_specific: False
              descriptor: MeanDescriptor
              descriptor_column: "mean"
              criteria:
                - RangeCriterion:
                    min_value: 0
                    max_value: 5

            - name: my_float_measurement
              input_source: "/my_table/my_data_float"
              input_source_subtype: my_subtype
              array_element: LST
              telescope_specific: False
              descriptor: Descriptor
              descriptor_column: "data"
              criteria:
                - RangeCriterion:
                    min_value: 0.3
                    max_value: 0.4
    """

    input_url = Path(
        help="URL of the input file containing monitoring data",
        directory_ok=False,
        file_ok=True,
    ).tag(config=True)

    data_category = TraitEnum(
        DataCategory,
        help="Data category (DataLevel/DataType)",
    ).tag(config=True)

    metrics = List(help="List of configs for metrics", minlen=1).tag(config=True)

    overwrite = Bool(
        default_value=False,
        help="Whether to overwrite existing quality monitoring tables in the input file",
    ).tag(config=True)

    aliases = {
        ("i", "input-url"): "MetricEvaluator.input_url",
        "data-category": "MetricEvaluator.data_category",
    }

    flags = {
        "overwrite": (
            {"MetricEvaluator": {"overwrite": True}},
            "Overwrite existing quality monitoring tables in the input file if they already exist",
        ),
    }

    classes = [Metric]

    # Categories read directly via read_table (no MonitoringSource needed)
    EVENT_CATEGORIES = {DataCategory.DL0_EVENT, DataCategory.DL1_EVENT}

    # Categories that use a MonitoringSource and get_table()
    DATA_SOURCE_TYPE_MAP = {
        DataCategory.DL1_MONITORING: HDF5MonitoringSource,
    }

    def setup(self):
        """Set up the MetricEvaluator class."""
        if self.input_url is None:
            raise ValueError("Input URL must be provided for MetricEvaluator")
        category = DataCategory(self.data_category)
        if category in self.EVENT_CATEGORIES:
            self._monitoring_source = None
        else:
            source_cls = self.DATA_SOURCE_TYPE_MAP[category]
            self._monitoring_source = source_cls(
                input_files=[self.input_url], parent=self
            )
        self._metrics = []
        for metric_dict in self.metrics:
            self.log.debug("MetricEvaluator : Loading metric %s", metric_dict)
            metric_cfg = Config(metric_dict)
            if metric_dict["Metric"].get("telescope_specific", False):
                if self._monitoring_source is not None:
                    subarray = self._monitoring_source.subarray
                else:
                    subarray = SubarrayDescription.from_hdf(self.input_url)
                self._metrics.append(
                    Metric(
                        config=Config(metric_cfg),
                        subarray=subarray,
                    )
                )
            else:
                self._metrics.append(Metric(config=Config(metric_cfg)))

    def _read_data(self, metric, tel_id=None):
        """
        Read data from the input source.

        For event categories (DL0_EVENT, DL1_EVENT), reads the HDF5 table directly
        via ``read_table`` using ``metric.input_source`` as the HDF5 path.

        For monitoring categories (DL1_MONITORING, DL0_MONITORING), delegates to
        ``MonitoringSource.get_table`` using ``metric.input_source`` as the
        ``MonitoringType`` value string.

        Parameters
        ----------
        metric : Metric
            Metric object with configuration
        tel_id : int, optional
            Telescope ID for telescope-specific metrics

        Returns
        -------
        astropy.table.Table
            Table with event-wise data or monitoring data:
            - DL1 event data: 'event_id', 'time', and data column
            - DL1_MONITORING: 'time_start', 'time_end', 'event_id_start', 'event_id_end', and data column
            - DL0_MONITORING: 'time' and data column
        """
        if DataCategory(self.data_category) in self.EVENT_CATEGORIES:
            table_path = metric.input_source
            if metric.telescope_specific and tel_id is not None:
                table_path = f"{table_path}/tel_{tel_id:03d}"
            table = read_table(self.input_url, path=table_path)
            if "time" not in table.colnames:
                trigger = read_table(self.input_url, path=DL1_SUBARRAY_TRIGGER_TABLE)
                table = astropy_join(
                    trigger[["obs_id", "event_id", "time"]],
                    table,
                    keys=["obs_id", "event_id"],
                )
            return table
        return self._monitoring_source.get_table(
            MonitoringType(metric.input_source),
            tel_id=tel_id,
            subtype=metric.input_source_subtype,
        )

    def start(self):
        """Loop over all metrics and apply criteria."""
        self.results = {}
        for metric in self._metrics:
            self.results[metric.name] = {}
            if metric.telescope_specific:
                for tel_id in metric.subarray.tel_ids:
                    data = self._read_data(metric, tel_id)
                    result_table = metric(data, tel_id)
                    id_info = f"{metric.array_element}-{tel_id:03d}"
                    self.results[metric.name][id_info] = result_table
            else:
                data = self._read_data(metric)
                result_table = metric(data)
                self.results[metric.name][metric.array_element] = result_table

    def finish(self):
        """Finish the job and write quality monitoring tables to HDF5."""
        self.log.debug("MetricEvaluator : Results: %s", self.results)

        # Write quality monitoring tables to HDF5
        self._write_monitoring_tables()

        self.log.info("MetricEvaluator : Finish running automatic QualPipe")

    def _write_monitoring_tables(self):
        """Write quality assessment results as monitoring tables to the input HDF5 file."""
        for metric in self._metrics:
            metric_results = self.results[metric.name]

            if metric.telescope_specific:
                # Write per-telescope monitoring tables
                for tel_id in metric.subarray.tel_ids:
                    id_info = f"{metric.array_element}-{tel_id:03d}"
                    if id_info in metric_results:
                        result_table = metric_results[id_info]
                        table_path = (
                            f"{DL1_TEL_QUALITY_GROUP}/{metric.name}/tel_{tel_id:03d}"
                        )
                        write_table(
                            result_table,
                            self.input_url,
                            path=table_path,
                            overwrite=self.overwrite,
                        )
                        self.log.info(
                            "Wrote monitoring table for metric '%s' telescope %d to %s",
                            metric.name,
                            tel_id,
                            table_path,
                        )
            else:
                # Write subarray-level monitoring table
                array_element = metric.array_element
                if array_element in metric_results:
                    result_table = metric_results[array_element]
                    table_path = (
                        f"{DL1_SUBARRAY_QUALITY_GROUP}/{array_element}/{metric.name}"
                    )
                    write_table(
                        result_table,
                        self.input_url,
                        path=table_path,
                        overwrite=self.overwrite,
                    )
                    self.log.info(
                        "Wrote monitoring table for metric '%s' to %s",
                        metric.name,
                        table_path,
                    )


def main():
    """Run the MetricEvaluator tool."""
    tool = MetricEvaluator()
    tool.run()
