# ----------------------------------------------------------------------
# Copyright (c) 2024 Rafael Gonzalez.
#
# See the LICENSE file for details
# ----------------------------------------------------------------------

# --------------------
# System wide imports
# -------------------

import sys
from abc import ABC, abstractmethod
import logging
import asyncio
from typing import Mapping, Any, Dict, Tuple, AsyncIterator


# ---------------------------
# Third-party library imports
# ----------------------------

from lica.asyncio.photometer import Role, Message as PhotMessage, Model as PhotModel, Sensor

# --------------
# local imports
# -------------

from .. import load_config
from ...dao import engine, Session
from .builder import PhotometerBuilder

# ----------------
# Module constants
# ----------------


SECTION = {Role.REF: "ref-device", Role.TEST: "test-device"}

# -----------------------
# Module global variables
# -----------------------

# get the module logger
log = logging.getLogger(__name__.split(".")[-1])

# -------------------
# Auxiliary functions
# -------------------

# -----------------
# Auxiliary classes
# -----------------


class Controller(ABC):
    """
    Reader Controller specialized in reading the photometers
    Serves ans an interface and a base class at the same time.
    """

    def __init__(
        self,
        ref_params: Mapping[str, Any] | None = None,
        test_params: Mapping[str, Any] | None = None,
    ):
        self.param = {Role.REF: ref_params, Role.TEST: test_params}
        self.roles = list()
        self.photometer = dict()
        self.ring = dict()
        self.phot_info = dict()
        self.phot_task = dict()
        if ref_params is not None:
            self.roles.append(Role.REF)
        if test_params is not None:
            self.roles.append(Role.TEST)

    # ==========
    # Public API
    # ==========

    def buffer(self, role: Role):
        return self.ring[role]

    async def init(self) -> None:
        log.info(
            "Initializing %s controller for %s",
            self.__class__.__name__,
            self.roles,
        )
        # Use engine parameter for the reference photometer when using database info
        builder = PhotometerBuilder(engine)
        async with Session() as session:
            for role in self.roles:
                val_db = await load_config(session, SECTION[role], "model")
                val_arg = self.param[role]["model"]
                self.param[role]["model"] = val_arg if val_arg is not None else PhotModel(val_db)
                val_db = await load_config(session, SECTION[role], "sensor")
                val_arg = self.param[role]["sensor"]
                self.param[role]["sensor"] = val_arg if val_arg is not None else Sensor(val_db)
                val_db = await load_config(session, SECTION[role], "old-proto")
                val_arg = self.param[role]["old_proto"]
                self.param[role]["old_proto"] = val_arg if val_arg is not None else bool(val_db)
                val_db = await load_config(session, SECTION[role], "endpoint")
                val_arg = self.param[role]["endpoint"]
                self.param[role]["endpoint"] = val_arg if val_arg is not None else val_db
                self.photometer[role] = builder.build(
                    self.param[role]["model"],
                    role,
                    self.param[role]["endpoint"],
                    self.param[role]["strict"],
                )
                logging.getLogger(str(role)).setLevel(self.param[role]["log_level"])

    async def info(self, role: Role) -> Dict[str, Any]:
        log = logging.getLogger(role.tag())
        try:
            phot_info = await self.photometer[role].get_info()
        except asyncio.exceptions.TimeoutError:
            log.error("Failed contacting %s photometer", role.tag())
            raise
        else:
            phot_info["endpoint"] = role.endpoint()
            phot_info["sensor"] = phot_info["sensor"] or self.param[role]["sensor"].value
            v = phot_info["freq_offset"] or 0.0
            phot_info["freq_offset"] = float(v)
            phot_info["zp"] = float(phot_info["zp"]) # ref photometer zp has a string, coming from database
            self.phot_info[role] = phot_info
            return phot_info

    async def readings(
        self, role: Role, num_messages: int | None = None
    ) -> AsyncIterator[Tuple[Role, PhotMessage]]:
        """An asynchronous generator, to be used by clients with async for"""
        num_messages = num_messages or sys.maxsize
        i = 0
        async with self.photometer[role]:
            while i < num_messages:
                reading = await anext(self.photometer[role].readings)
                if reading is not None:
                    i += 1
                    yield role, reading

    async def write_zp(self, zero_point: float) -> float:
        """May raise asyncio.exceptions.TimeoutError in particular"""
        await self.photometer[Role.TEST].save_zero_point(zero_point)
        stored_zero_point = (await self.photometer[Role.TEST].get_info())["zp"]
        return stored_zero_point

    @abstractmethod
    async def calibrate(self) -> float:
        """Calibrate the test photometer against the refrence photometer returnoing a Zero Point"""
        pass
