from __future__ import annotations

import abc
import random
import threading
import time
from typing import Callable, List, Optional

from ..actions import Action, ActionKind, LockStrategy
from ..addressing import Addressing
from ..automotive import NoResponseFromBus, UdsNegativeResponse
from ..message import Message
from ..routine import Routine
from ..service import Service
from ..uds import DiagnosticSessionType, NegativeResponseCode


class SwallowResponse(Exception):
    pass


class OverrideReply(Exception):
    def __init__(self, reply_id: int, bytes_: List[int]) -> None:
        self.reply_id = reply_id
        self.bytes = bytes_


class CommandHandler(abc.ABC):
    @property
    @abc.abstractmethod
    def arbitration_id(self) -> int:
        raise NotImplementedError

    @property
    @abc.abstractmethod
    def reply_id(self) -> int:
        raise NotImplementedError

    @property
    @abc.abstractmethod
    def request_extension(self) -> Optional[int]:
        raise NotImplementedError

    @property
    @abc.abstractmethod
    def reply_extension(self) -> Optional[int]:
        raise NotImplementedError

    @property
    @abc.abstractmethod
    def auxiliary_ids(self) -> List[int]:
        raise NotImplementedError

    @property
    @abc.abstractmethod
    def auxiliary_request_extensions(self) -> List[int]:
        raise NotImplementedError

    @property
    @abc.abstractmethod
    def p2_server_max(self) -> float:
        raise NotImplementedError

    @property
    @abc.abstractmethod
    def p2ext_server_max(self) -> float:
        raise NotImplementedError

    def register(self, ecu) -> None:
        return

    def unregister(self, ecu) -> None:
        return

    def configure(self, property_name: str, value: str) -> None:
        return

    @abc.abstractmethod
    def handle(self, message: Message, sink: Callable[[Message], None]) -> Optional[Message]:
        raise NotImplementedError


class DiagnosticSession(CommandHandler):
    default_session: DiagnosticSessionType = DiagnosticSessionType.default

    def __init__(
        self,
        arbitration_id: int,
        reply_id: int,
        auxiliary_ids: List[int],
        request_extension: Optional[int],
        reply_extension: Optional[int],
        auxiliary_request_extensions: List[int],
        p2_server_max: float,
        p2ext_server_max: float,
        s3_session_timeout: float,
        memory: List,
        ecu_identifiers: List,
        local_identifiers: List,
        identifiers: List,
        routines: List[Routine],
        security_levels: List,
        custom_services: List[Service],
        dtcs: List,
        dtcs_clearing_delay: float,
        startup_actions: List[Action],
    ) -> None:
        self.ecu = None
        self.reset_arbitration_id = arbitration_id
        self.reset_reply_id = reply_id
        self.reset_request_extension = request_extension
        self.reset_reply_extension = reply_extension
        self.reset_auxiliary_ids = auxiliary_ids
        self.reset_auxiliary_request_extensions = auxiliary_request_extensions

        self._arbitration_id = arbitration_id
        self._reply_id = reply_id
        self._request_extension = request_extension
        self._reply_extension = reply_extension
        self._auxiliary_ids = auxiliary_ids
        self._auxiliary_request_extensions = auxiliary_request_extensions

        self._p2_server_max = p2_server_max
        self._p2ext_server_max = p2ext_server_max
        self.s3_session_timeout = s3_session_timeout

        self.memory = memory
        self.security_levels = security_levels
        self.custom_services = custom_services
        self.dtcs = dtcs
        self.dtcs_clearing_delay = dtcs_clearing_delay
        self.startup_actions = startup_actions

        self.ecu_identifiers = {identifier.identifier: identifier for identifier in ecu_identifiers}
        self.local_identifiers = {identifier.identifier: identifier for identifier in local_identifiers}
        self.identifiers = {identifier.identifier: identifier for identifier in identifiers}
        self.dynamic_identifiers = {}

        combined_routines = {}
        for routine in routines:
            if routine.identifier in combined_routines:
                existing = combined_routines[routine.identifier]
                existing.responses.extend(routine.responses)
            else:
                combined_routines[routine.identifier] = routine
        self.routines = combined_routines

        self.current = self.default_session
        self.security_level = 0
        self.seed_key_timestamp: float | None = None
        self.active_transfer = None

        self._session_timer: Optional[threading.Timer] = None
        self._command_timer: Optional[threading.Timer] = None

        self.perform_session_setup()
        threading.Timer(0.2, self.perform_actions, args=[self.startup_actions], kwargs={"source": "startup"}).start()

    @property
    def arbitration_id(self) -> int:
        return self._arbitration_id

    @property
    def reply_id(self) -> int:
        return self._reply_id

    @property
    def request_extension(self) -> Optional[int]:
        return self._request_extension

    @property
    def reply_extension(self) -> Optional[int]:
        return self._reply_extension

    @property
    def auxiliary_ids(self) -> List[int]:
        return self._auxiliary_ids

    @property
    def auxiliary_request_extensions(self) -> List[int]:
        return self._auxiliary_request_extensions

    @property
    def p2_server_max(self) -> float:
        return self._p2_server_max

    @property
    def p2ext_server_max(self) -> float:
        return self._p2ext_server_max

    def register(self, ecu) -> None:
        self.ecu = ecu

    def unregister(self, ecu) -> None:
        self.ecu = None

    def reset_default_session_timer(self) -> None:
        if self._session_timer:
            self._session_timer.cancel()
            self._session_timer = None
        if self.current != self.default_session:
            def _timeout() -> None:
                self.current = self.default_session
                for action in self.startup_actions:
                    if action.kind == ActionKind.lock_all_other_ecus:
                        self.perform_action(action)
            self._session_timer = threading.Timer(self.s3_session_timeout, _timeout)
            self._session_timer.daemon = True
            self._session_timer.start()

    def perform_session_setup(self) -> None:
        return

    def perform_actions(self, actions: List[Action], source: str = "actions") -> None:
        for action in actions:
            self._log_action(action, source)
            self.perform_action(action)

    def _log_action(self, action: Action, source: str) -> None:
        is_startup = source == "startup"
        is_firewall = action.kind in (ActionKind.lock_all_other_ecus, ActionKind.unlock_all_other_ecus)
        if not is_startup and not is_firewall:
            return
        ecu_name = self.ecu.name if self.ecu else "ECU"
        prefix = "Startup action" if is_startup else "Action"
        details = action.kind.value
        if action.kind == ActionKind.lock_all_other_ecus and action.lock_strategy is not None:
            details = f"{details} (strategy={action.lock_strategy.value})"
        arbitration = f"0x{self.arbitration_id:X}"
        reply = f"0x{self.reply_id:X}"
        print(f"[actions] {ecu_name} ({arbitration}->{reply}): {prefix}: {details}")

    def perform_action(self, action: Action) -> None:
        if action.kind == ActionKind.set_arbitration:
            self._arbitration_id = action.request_id or self._arbitration_id
            self._reply_id = action.reply_id or self._reply_id
            self._auxiliary_ids = action.auxiliary_ids or self._auxiliary_ids
        elif action.kind == ActionKind.host:
            if self.ecu and action.host_action:
                self.ecu.perform_host_action(action.host_action)
        elif action.kind == ActionKind.lock_all_other_ecus:
            if self.ecu:
                self.ecu.lock_all_other_ecus(action.lock_strategy or LockStrategy.default)
                self.s3_session_timeout = 5.0
        elif action.kind == ActionKind.unlock_all_other_ecus:
            if self.ecu:
                self.ecu.unlock_all_other_ecus()
                self.s3_session_timeout = 2.0
        elif action.kind == ActionKind.override_reply_id:
            return

    def perform_reset(self) -> None:
        self._arbitration_id = self.reset_arbitration_id
        self._reply_id = self.reset_reply_id
        self._request_extension = self.reset_request_extension
        self._reply_extension = self.reset_reply_extension
        self._auxiliary_ids = self.reset_auxiliary_ids
        self._auxiliary_request_extensions = self.reset_auxiliary_request_extensions
        self.current = self.default_session
        time.sleep(2.0)

    def handle_custom_service(self, service: Service, message: Message) -> List[int]:
        if random.random() < service.debug_swallow_response_likeliness:
            return []
        time.sleep(service.delay / 1000.0)
        replay_response = service.next_replay_response()
        if replay_response is not None:
            return replay_response
        if service.response is not None:
            return service.response
        response_bytes = [message.bytes[0] | 0x40]
        if len(message.bytes) > 1:
            response_bytes.append(message.bytes[1])
        override_reply = None
        for action in service.actions:
            if action.kind == ActionKind.override_reply_id:
                override_reply = action.reply_id
        if override_reply is not None:
            raise OverrideReply(override_reply, response_bytes)
        return response_bytes

    def handle(self, message: Message, sink: Callable[[Message], None]) -> Optional[Message]:
        message = message.with_reply_id(self.reply_id)

        stop_event = threading.Event()

        def pending_tick() -> None:
            if stop_event.is_set():
                return
            intermediate = message.response_pending()
            sink(intermediate)
            self.reset_default_session_timer()
            interval = max(self.p2ext_server_max - 0.1, 0.1)
            self._command_timer = threading.Timer(interval, pending_tick)
            self._command_timer.daemon = True
            self._command_timer.start()

        self._command_timer = threading.Timer(0.1, pending_tick)
        self._command_timer.daemon = True
        self._command_timer.start()

        try:
            if self.custom_services:
                for custom in self.custom_services:
                    if custom.matches_request(message.bytes):
                        response_bytes = self.handle_custom_service(custom, message)
                        if not response_bytes:
                            return None
                        return message.response(response_bytes)
            response_bytes = self.handle_request(message)
            if not response_bytes:
                return None
            return message.response(response_bytes)
        except SwallowResponse:
            return None
        except OverrideReply as override:
            addressing = Addressing.id_only(override.reply_id, extension=self.reply_extension or 0)
            return Message(addressing=addressing, bytes=override.bytes)
        except NoResponseFromBus:
            return None
        except UdsNegativeResponse as exc:
            return message.negative_response(exc.code)
        finally:
            stop_event.set()
            if self._command_timer:
                self._command_timer.cancel()
                self._command_timer = None

    def handle_request(self, message: Message) -> List[int]:
        raise UdsNegativeResponse(NegativeResponseCode.service_not_supported)
