from __future__ import annotations

import random
import threading
import time
from typing import List, Tuple

from .base import DiagnosticSession, SwallowResponse, OverrideReply
from ..actions import ActionKind
from ..automotive import (
    DynamicIdentifier,
    Identifier,
    MemoryBlock,
    NoResponseFromBus,
    UdsNegativeResponse,
)
from ..uds import (
    DiagnosticSessionType,
    NegativeResponseCode,
    RoutineControlType,
    TesterPresentType,
    DynamicallyDefineDataIdentifierDefinitionType,
)


class CommonDiagnosticSession(DiagnosticSession):
    def clear_diagnostic_information(self, group_of_dtc: int) -> List[int]:
        self.reset_default_session_timer()
        time.sleep(self.dtcs_clearing_delay)
        self.dtcs.clear()
        return []

    def clear_dynamically_defined_identifier(self, identifier: int) -> List[int]:
        self.reset_default_session_timer()
        if identifier == 0x0000:
            self.dynamic_identifiers.clear()
            return [int(DynamicallyDefineDataIdentifierDefinitionType.clear)]
        if not (0xF300 <= identifier <= 0xF3FF):
            raise UdsNegativeResponse(NegativeResponseCode.request_out_of_range)
        self.dynamic_identifiers.pop(identifier, None)
        return [int(DynamicallyDefineDataIdentifierDefinitionType.clear), (identifier >> 8) & 0xFF, identifier & 0xFF]

    def control_dtc_setting(self, control_type: int, record: List[int]) -> List[int]:
        self.reset_default_session_timer()
        return []

    def communication_control(self, control_type: int, communication_type: int) -> List[int]:
        self.reset_default_session_timer()
        return []

    def dynamically_define_identifier(self, identifier: int, definitions: List[Tuple[int, range]]) -> List[int]:
        self.reset_default_session_timer()
        if not definitions:
            raise UdsNegativeResponse(NegativeResponseCode.request_out_of_range)
        slices: List[tuple] = []
        for source_identifier, rng in definitions:
            if source_identifier not in self.identifiers:
                raise UdsNegativeResponse(NegativeResponseCode.request_out_of_range)
            source = self.identifiers[source_identifier]
            if rng.start < 0 or not source.has_range(rng):
                raise UdsNegativeResponse(NegativeResponseCode.request_out_of_range)
            slices.append(("identifier", (source_identifier, rng)))
        return self.register_dynamic_identifier(identifier, DynamicallyDefineDataIdentifierDefinitionType.define_by_identifier, slices)

    def register_dynamic_identifier(self, identifier: int, subfunction: int, slices: List[tuple]) -> List[int]:
        if not (0xF300 <= identifier <= 0xF3FF):
            raise UdsNegativeResponse(NegativeResponseCode.request_out_of_range)
        if not slices:
            raise UdsNegativeResponse(NegativeResponseCode.request_out_of_range)
        dynamic_identifier = self.dynamic_identifiers.get(identifier, DynamicIdentifier(identifier))
        if dynamic_identifier.slice_count + len(slices) > DynamicIdentifier.max_slices:
            raise UdsNegativeResponse(NegativeResponseCode.request_out_of_range)
        additional_length = 0
        for kind, payload in slices:
            if kind == "identifier":
                _, rng = payload
                additional_length += rng.stop - rng.start
            elif kind == "memory":
                _, _, length = payload
                additional_length += length
        if dynamic_identifier.total_length + additional_length > DynamicIdentifier.max_total_bytes:
            raise UdsNegativeResponse(NegativeResponseCode.request_out_of_range)
        dynamic_identifier.slices.extend(slices)
        self.dynamic_identifiers[identifier] = dynamic_identifier
        return [int(subfunction), (identifier >> 8) & 0xFF, identifier & 0xFF]

    def read_data_by(self, identifier: int) -> List[int]:
        self.reset_default_session_timer()
        if identifier in self.dynamic_identifiers:
            dynamic_identifier = self.dynamic_identifiers[identifier]
            data: List[int] = []
            for kind, payload in dynamic_identifier.slices:
                if kind == "identifier":
                    source_id, rng = payload
                    data.extend(self.read_data_by(source_id)[rng.start:rng.stop])
                elif kind == "memory":
                    block, offset, length = payload
                    data.extend(block.read(offset, length))
            return data
        if identifier not in self.identifiers:
            raise UdsNegativeResponse(NegativeResponseCode.request_out_of_range)
        identifier_obj = self.identifiers[identifier]
        if identifier_obj.read_levels and self.security_level not in identifier_obj.read_levels:
            raise UdsNegativeResponse(NegativeResponseCode.security_access_denied)
        content = identifier_obj.content
        if not content:
            return [0x00] * identifier_obj.data_type.length
        return content

    def read_memory_by(self, address_bytes: List[int], length_bytes: List[int]) -> List[int]:
        self.reset_default_session_timer()
        if not length_bytes:
            raise UdsNegativeResponse(NegativeResponseCode.request_out_of_range)
        length = int.from_bytes(bytes(length_bytes), "big")
        if length <= 0:
            raise UdsNegativeResponse(NegativeResponseCode.request_out_of_range)
        if length >= 0xFFF:
            raise UdsNegativeResponse(NegativeResponseCode.request_out_of_range)
        address_value = int.from_bytes(bytes(address_bytes), "big")
        block = next((b for b in self.memory if address_value >= b.address and address_value + length <= b.address + b.size), None)
        if block is None:
            return [0xFF] * length
        offset = int(address_value - block.address)
        return block.read(offset, length)

    def read_scaling_data_by(self, identifier: int) -> List[int]:
        self.reset_default_session_timer()
        return self.read_data_by(identifier)

    def write_memory_by(self, address_bytes: List[int], length_bytes: List[int], data: List[int]) -> List[int]:
        self.reset_default_session_timer()
        if not length_bytes:
            raise UdsNegativeResponse(NegativeResponseCode.request_out_of_range)
        length = int.from_bytes(bytes(length_bytes), "big")
        if length <= 0:
            raise UdsNegativeResponse(NegativeResponseCode.request_out_of_range)
        if length >= 0xFFF:
            raise UdsNegativeResponse(NegativeResponseCode.request_out_of_range)
        if length != len(data):
            raise UdsNegativeResponse(NegativeResponseCode.incorrect_message_length_or_invalid_format)
        address_value = int.from_bytes(bytes(address_bytes), "big")
        block = next((b for b in self.memory if address_value >= b.address and address_value + length <= b.address + b.size), None)
        if block is None:
            raise UdsNegativeResponse(NegativeResponseCode.request_out_of_range)
        offset = int(address_value - block.address)
        block.write(offset, data)
        return address_bytes + length_bytes

    def request_download(self, compression: int, encryption: int, address: List[int], length: List[int]) -> List[int]:
        self.reset_default_session_timer()
        if self.current == self.default_session:
            raise UdsNegativeResponse(NegativeResponseCode.service_not_supported_in_active_session)
        address_value = int.from_bytes(bytes(address), "big")
        length_value = int.from_bytes(bytes(length), "big")
        block = next((b for b in self.memory if b.address == address_value and b.size == length_value), None)
        if block is None:
            raise UdsNegativeResponse(NegativeResponseCode.request_out_of_range)
        block.start_download()
        self.active_transfer = block
        mtu = block.mtu
        if mtu <= 0 or mtu > 0x0FFF:
            raise UdsNegativeResponse(NegativeResponseCode.request_out_of_range)
        return [0x20, (mtu >> 8) & 0xFF, mtu & 0xFF]

    def request_upload(self, compression: int, encryption: int, address: List[int], length: List[int]) -> List[int]:
        self.reset_default_session_timer()
        if self.current == self.default_session:
            raise UdsNegativeResponse(NegativeResponseCode.service_not_supported_in_active_session)
        address_value = int.from_bytes(bytes(address), "big")
        length_value = int.from_bytes(bytes(length), "big")
        block = next((b for b in self.memory if b.address == address_value and b.size == length_value), None)
        if block is None:
            raise UdsNegativeResponse(NegativeResponseCode.request_out_of_range)
        block.start_upload()
        self.active_transfer = block
        mtu = block.mtu
        if mtu <= 0 or mtu > 0x0FFF:
            raise UdsNegativeResponse(NegativeResponseCode.request_out_of_range)
        return [0x20, (mtu >> 8) & 0xFF, mtu & 0xFF]

    def request_transfer_exit(self, record: List[int]) -> List[int]:
        self.reset_default_session_timer()
        if self.active_transfer is None:
            raise UdsNegativeResponse(NegativeResponseCode.request_sequence_error)
        if self.active_transfer.has_incoming_transfer:
            self.active_transfer.exit_download()
        else:
            self.active_transfer.exit_upload()
        self.active_transfer = None
        return []

    def reset(self, reset_type: int) -> List[int]:
        self.reset_default_session_timer()
        self.perform_reset()
        return [int(reset_type)]

    def routine_control(self, control_type: int, identifier: int, record: List[int]) -> List[int]:
        self.reset_default_session_timer()
        routine = self.routines.get(identifier)
        if routine is None:
            raise UdsNegativeResponse(NegativeResponseCode.request_out_of_range)
        if routine.security_levels and self.security_level not in routine.security_levels:
            raise UdsNegativeResponse(NegativeResponseCode.security_access_denied)
        if routine.ignore_option_record:
            response = routine.responses[0] if routine.responses else None
        else:
            response = next((r for r in routine.responses if r.option_record == record), None)
        if response is None:
            raise UdsNegativeResponse(NegativeResponseCode.request_out_of_range)
        if random.random() < routine.debug_swallow_response_likeliness:
            return []
        if control_type == RoutineControlType.start_routine:
            if response.delay > 0:
                time.sleep(response.delay / 1000.0)
            threading.Timer(0.5, self.perform_actions, args=[routine.actions]).start()
            return response.start_response
        if control_type == RoutineControlType.stop_routine:
            if response.stop_response is None:
                raise UdsNegativeResponse(NegativeResponseCode.sub_function_not_supported)
            return response.stop_response
        if control_type == RoutineControlType.request_routine_results:
            if response.results_response is None:
                raise UdsNegativeResponse(NegativeResponseCode.sub_function_not_supported)
            return response.results_response
        raise UdsNegativeResponse(NegativeResponseCode.sub_function_not_supported)

    def session_control(self, session_type: int) -> List[int]:
        self.reset_default_session_timer()
        self.current = DiagnosticSessionType(session_type) if session_type in DiagnosticSessionType._value2member_map_ else DiagnosticSessionType.undefined
        time.sleep(0.5)
        p2sm = int(self.p2_server_max * 1000)
        p2esm = int(self.p2ext_server_max * 100)
        return [session_type, (p2sm >> 8) & 0xFF, p2sm & 0xFF, (p2esm >> 8) & 0xFF, p2esm & 0xFF]

    def security_access_request_seed(self, level: int) -> List[int]:
        self.reset_default_session_timer()
        security_level = next((s for s in self.security_levels if s.level == level), None)
        if security_level is None:
            raise UdsNegativeResponse(NegativeResponseCode.request_out_of_range)
        self.seed_key_timestamp = time.time()
        seed = random.choice(security_level.seed_key_pairs).seed
        return [level] + list(seed)

    def security_access_send_key(self, level: int, key: List[int]) -> List[int]:
        self.reset_default_session_timer()
        if self.seed_key_timestamp is None:
            raise UdsNegativeResponse(NegativeResponseCode.request_sequence_error)
        self.seed_key_timestamp = None
        query_level = level - 1
        security_level = next((s for s in self.security_levels if s.level == query_level), None)
        if security_level is None:
            raise UdsNegativeResponse(NegativeResponseCode.request_out_of_range)
        if not any(pair.key == key for pair in security_level.seed_key_pairs):
            raise UdsNegativeResponse(NegativeResponseCode.invalid_key)
        if security_level.delay > 0:
            time.sleep(security_level.delay / 1000.0)
        self.security_level = query_level
        self.perform_actions(security_level.actions)
        override_reply = None
        for action in security_level.actions:
            if action.kind == ActionKind.override_reply_id:
                override_reply = action.reply_id
        if override_reply is not None:
            raise OverrideReply(override_reply, [0x27 | 0x40, level])
        return [level]

    def tester_present(self, present_type: int) -> List[int]:
        self.reset_default_session_timer()
        if present_type == TesterPresentType.do_not_respond:
            return []
        return [int(present_type)]

    def transfer_data(self, block_number: int, block_data: List[int]) -> List[int]:
        self.reset_default_session_timer()
        if not self.active_transfer or not self.active_transfer.has_active_transfer:
            raise UdsNegativeResponse(NegativeResponseCode.request_sequence_error)
        if block_number != self.active_transfer.next_block:
            raise UdsNegativeResponse(NegativeResponseCode.wrong_block_sequence_counter)
        if random.random() < self.active_transfer.debug_swallow_response_likeliness:
            raise SwallowResponse()
        if self.active_transfer.has_incoming_transfer:
            self.active_transfer.download(block_data)
            return [block_number]
        data = self.active_transfer.upload()
        return [block_number] + data

    def write_data_by(self, identifier: int, data_record: List[int]) -> List[int]:
        self.reset_default_session_timer()
        if identifier not in self.identifiers:
            raise UdsNegativeResponse(NegativeResponseCode.request_out_of_range)
        id_obj = self.identifiers[identifier]
        if id_obj.write_levels and self.security_level not in id_obj.write_levels:
            raise UdsNegativeResponse(NegativeResponseCode.security_access_denied)
        id_obj.content = data_record
        self.identifiers[identifier] = id_obj
        return [(identifier >> 8) & 0xFF, identifier & 0xFF]
