from __future__ import annotations

from typing import List

from .common import CommonDiagnosticSession
from ..automotive import DTCStatusMask, Identifier, UdsNegativeResponse
from ..kwp import ECUIdentificationEntry
from ..uds import (
    DiagnosticSessionType,
    DynamicallyDefineDataIdentifierDefinitionType,
    EcuResetType,
    NegativeResponseCode,
    RoutineControlType,
    ServiceId,
)
from ..uds import KWP as KWPConst


class KWPDiagnosticSession(CommonDiagnosticSession):
    default_session = DiagnosticSessionType.kwp_default
    timing_parameter_count = 3

    def __init__(self, *args, **kwargs) -> None:
        timing_parameters = kwargs.pop("kwp_timing_parameters", None)
        super().__init__(*args, **kwargs)
        if timing_parameters:
            normalized = list(timing_parameters)[: self.timing_parameter_count]
            while len(normalized) < self.timing_parameter_count:
                normalized.append(0x00)
            self._kwp_timing_parameters = normalized
        else:
            self._kwp_timing_parameters = [0x00] * self.timing_parameter_count
        self._kwp_pending_timing_parameters = None

    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
        return [session_type]

    def handle_request(self, message):
        bytes_in = message.bytes
        if not bytes_in:
            raise UdsNegativeResponse(NegativeResponseCode.incorrect_message_length_or_invalid_format)
        response_bytes = [bytes_in[0] | 0x40]
        sid = bytes_in[0]

        if self._kwp_pending_timing_parameters is not None:
            self._kwp_timing_parameters = self._kwp_pending_timing_parameters
            self._kwp_pending_timing_parameters = None

        if sid == ServiceId.control_dtc_setting:
            if len(bytes_in) < 2:
                raise UdsNegativeResponse(NegativeResponseCode.incorrect_message_length_or_invalid_format)
            response_bytes += self.control_dtc_setting(bytes_in[1], bytes_in[2:])
            return response_bytes

        if sid == ServiceId.clear_diagnostic_information:
            if len(bytes_in) != 3:
                raise UdsNegativeResponse(NegativeResponseCode.incorrect_message_length_or_invalid_format)
            group_of_dtc = int.from_bytes([0x00, 0x00] + bytes_in[1:3], "big")
            if group_of_dtc != 0xFFFF:
                raise UdsNegativeResponse(NegativeResponseCode.request_out_of_range)
            response_bytes += self.clear_diagnostic_information(group_of_dtc)
            return response_bytes

        if sid == ServiceId.diagnostic_session_control:
            if len(bytes_in) != 2:
                raise UdsNegativeResponse(NegativeResponseCode.incorrect_message_length_or_invalid_format)
            if bytes_in[1] not in DiagnosticSessionType._value2member_map_:
                raise UdsNegativeResponse(NegativeResponseCode.sub_function_not_supported)
            response_bytes += self.session_control(bytes_in[1])
            return response_bytes

        if sid == ServiceId.kwp_stop_diagnostic_session:
            if len(bytes_in) != 1:
                raise UdsNegativeResponse(NegativeResponseCode.incorrect_message_length_or_invalid_format)
            self.current = self.default_session
            self.reset_default_session_timer()
            return response_bytes

        if sid == ServiceId.dynamically_define_data_identifier:
            if len(bytes_in) <= 1:
                raise UdsNegativeResponse(NegativeResponseCode.incorrect_message_length_or_invalid_format)
            if bytes_in[1] not in DynamicallyDefineDataIdentifierDefinitionType._value2member_map_:
                raise UdsNegativeResponse(NegativeResponseCode.sub_function_not_supported)
            identifier = 0x0000
            if len(bytes_in) >= 4:
                identifier = (bytes_in[2] << 8) | bytes_in[3]
            subfunction = bytes_in[1]
            if subfunction == DynamicallyDefineDataIdentifierDefinitionType.define_by_identifier:
                if len(bytes_in) < 8:
                    raise UdsNegativeResponse(NegativeResponseCode.incorrect_message_length_or_invalid_format)
                record_length = 4
                payload_length = len(bytes_in) - 4
                if payload_length % record_length != 0:
                    raise UdsNegativeResponse(NegativeResponseCode.incorrect_message_length_or_invalid_format)
                definitions = []
                index = 4
                while index + record_length <= len(bytes_in):
                    source_id = (bytes_in[index] << 8) | bytes_in[index + 1]
                    lower_bound = bytes_in[index + 2] - 1
                    if lower_bound < 0:
                        raise UdsNegativeResponse(NegativeResponseCode.request_out_of_range)
                    length = bytes_in[index + 3]
                    if length <= 0:
                        raise UdsNegativeResponse(NegativeResponseCode.request_out_of_range)
                    definitions.append((source_id, range(lower_bound, lower_bound + length)))
                    index += record_length
                response_bytes += self.dynamically_define_identifier(identifier, definitions)
                return response_bytes
            if subfunction == DynamicallyDefineDataIdentifierDefinitionType.define_by_memory_address:
                if len(bytes_in) <= 5:
                    raise UdsNegativeResponse(NegativeResponseCode.incorrect_message_length_or_invalid_format)
                format_spec = bytes_in[4]
                number_of_address_bytes = format_spec & 0x0F
                number_of_length_bytes = format_spec >> 4
                if number_of_address_bytes == 0 or number_of_length_bytes == 0:
                    raise UdsNegativeResponse(NegativeResponseCode.incorrect_message_length_or_invalid_format)
                index = 5
                slices = []
                record_length = number_of_address_bytes + number_of_length_bytes
                while index + record_length <= len(bytes_in):
                    address_bytes = bytes_in[index:index + number_of_address_bytes]
                    index += number_of_address_bytes
                    length_bytes = bytes_in[index:index + number_of_length_bytes]
                    index += number_of_length_bytes
                    address = int.from_bytes(bytes(address_bytes), "big")
                    length = int.from_bytes(bytes(length_bytes), "big")
                    if length <= 0:
                        raise UdsNegativeResponse(NegativeResponseCode.request_out_of_range)
                    block = next((b for b in self.memory if address >= b.address and address + length <= b.address + b.size), None)
                    if block is None:
                        raise UdsNegativeResponse(NegativeResponseCode.request_out_of_range)
                    offset = int(address - block.address)
                    slices.append(("memory", (block, offset, int(length))))
                if index != len(bytes_in) or not slices:
                    raise UdsNegativeResponse(NegativeResponseCode.incorrect_message_length_or_invalid_format)
                response_bytes += self.register_dynamic_identifier(identifier, subfunction, slices)
                return response_bytes
            if subfunction == DynamicallyDefineDataIdentifierDefinitionType.clear:
                if len(bytes_in) not in (2, 4):
                    raise UdsNegativeResponse(NegativeResponseCode.incorrect_message_length_or_invalid_format)
                response_bytes += self.clear_dynamically_defined_identifier(identifier)
                return response_bytes

        if sid == ServiceId.ecu_reset:
            if len(bytes_in) != 2:
                raise UdsNegativeResponse(NegativeResponseCode.incorrect_message_length_or_invalid_format)
            if bytes_in[1] not in EcuResetType._value2member_map_:
                raise UdsNegativeResponse(NegativeResponseCode.sub_function_not_supported)
            response_bytes += self.reset(bytes_in[1])
            return response_bytes

        if sid == ServiceId.kwp_read_freeze_frame_data:
            if len(bytes_in) != 1:
                raise UdsNegativeResponse(NegativeResponseCode.incorrect_message_length_or_invalid_format)
            return response_bytes

        if sid == ServiceId.kwp_read_diagnostic_trouble_code:
            if len(bytes_in) < 1:
                raise UdsNegativeResponse(NegativeResponseCode.incorrect_message_length_or_invalid_format)
            response_bytes += self.read_diagnostic_trouble_codes_by_status()
            return response_bytes

        if sid == ServiceId.kwp_read_dtcs_by_status:
            if len(bytes_in) != 4:
                raise UdsNegativeResponse(NegativeResponseCode.incorrect_message_length_or_invalid_format)
            if bytes_in[1] != 0x02:
                raise UdsNegativeResponse(NegativeResponseCode.sub_function_not_supported)
            if bytes_in[2] != 0xFF or bytes_in[3] != 0xFF:
                raise UdsNegativeResponse(NegativeResponseCode.request_out_of_range)
            response_bytes += self.read_diagnostic_trouble_codes_by_status()
            return response_bytes

        if sid == ServiceId.kwp_read_status_of_dtcs:
            if len(bytes_in) != 2:
                raise UdsNegativeResponse(NegativeResponseCode.incorrect_message_length_or_invalid_format)
            status_mask = bytes_in[1]
            response_bytes += self.read_dtc_by_status_mask(status_mask)
            return response_bytes

        if sid == ServiceId.read_data_by_identifier:
            if len(bytes_in) < 3 or (len(bytes_in) - 1) % 2 != 0:
                raise UdsNegativeResponse(NegativeResponseCode.incorrect_message_length_or_invalid_format)
            index = 1
            while index + 1 < len(bytes_in):
                identifier = (bytes_in[index] << 8) | bytes_in[index + 1]
                response_bytes += [bytes_in[index], bytes_in[index + 1]]
                response_bytes += self.read_data_by(identifier)
                index += 2
            return response_bytes

        if sid == ServiceId.read_memory_by_address:
            if len(bytes_in) != 5:
                raise UdsNegativeResponse(NegativeResponseCode.incorrect_message_length_or_invalid_format)
            address = bytes_in[1:4]
            length = [bytes_in[4]]
            response_bytes += self.read_memory_by(address, length)
            return response_bytes

        if sid == ServiceId.read_scaling_data_by_identifier:
            if len(bytes_in) < 3 or (len(bytes_in) - 1) % 2 != 0:
                raise UdsNegativeResponse(NegativeResponseCode.incorrect_message_length_or_invalid_format)
            index = 1
            while index + 1 < len(bytes_in):
                identifier = (bytes_in[index] << 8) | bytes_in[index + 1]
                response_bytes += [bytes_in[index], bytes_in[index + 1]]
                response_bytes += self.read_scaling_data_by(identifier)
                index += 2
            return response_bytes

        if sid == ServiceId.request_download:
            if len(bytes_in) < 5 or len(bytes_in) > 33:
                raise UdsNegativeResponse(NegativeResponseCode.incorrect_message_length_or_invalid_format)
            data_format = bytes_in[1]
            compression = data_format >> 4
            encryption = data_format & 0x0F
            format_spec = bytes_in[2]
            number_of_length_bytes = format_spec >> 4
            number_of_address_bytes = format_spec & 0x0F
            expected = 3 + number_of_address_bytes + number_of_length_bytes
            if len(bytes_in) != expected:
                raise UdsNegativeResponse(NegativeResponseCode.incorrect_message_length_or_invalid_format)
            address = bytes_in[3:3 + number_of_address_bytes]
            length = bytes_in[3 + number_of_address_bytes:expected]
            response_bytes += self.request_download(compression, encryption, address, length)
            return response_bytes

        if sid == ServiceId.request_transfer_exit:
            response_bytes += self.request_transfer_exit(bytes_in[1:])
            return response_bytes

        if sid == ServiceId.kwp_start_communication:
            if len(bytes_in) < 1:
                raise UdsNegativeResponse(NegativeResponseCode.incorrect_message_length_or_invalid_format)
            if len(bytes_in) > 1:
                response_bytes += bytes_in[1:]
            return response_bytes

        if sid == ServiceId.kwp_stop_communication:
            if len(bytes_in) < 1:
                raise UdsNegativeResponse(NegativeResponseCode.incorrect_message_length_or_invalid_format)
            if len(bytes_in) > 1:
                response_bytes += bytes_in[1:]
            return response_bytes

        if sid == ServiceId.kwp_access_timing_parameters:
            if len(bytes_in) < 2:
                raise UdsNegativeResponse(NegativeResponseCode.incorrect_message_length_or_invalid_format)
            timing_parameter_index = bytes_in[1]
            if len(bytes_in) == 2:
                response_bytes += [timing_parameter_index] + list(self._kwp_timing_parameters)
                return response_bytes
            expected_length = 2 + self.timing_parameter_count
            if len(bytes_in) != expected_length:
                raise UdsNegativeResponse(NegativeResponseCode.incorrect_message_length_or_invalid_format)
            self._kwp_pending_timing_parameters = list(bytes_in[2:expected_length])
            response_bytes += [timing_parameter_index]
            return response_bytes

        if sid == ServiceId.kwp_network_configuration:
            if len(bytes_in) < 1:
                raise UdsNegativeResponse(NegativeResponseCode.incorrect_message_length_or_invalid_format)
            if len(bytes_in) > 1:
                response_bytes += bytes_in[1:]
            return response_bytes

        if sid == ServiceId.routine_control:
            if len(bytes_in) < 4:
                raise UdsNegativeResponse(NegativeResponseCode.incorrect_message_length_or_invalid_format)
            if bytes_in[1] not in RoutineControlType._value2member_map_:
                raise UdsNegativeResponse(NegativeResponseCode.sub_function_not_supported)
            identifier = (bytes_in[2] << 8) | bytes_in[3]
            option_record = bytes_in[4:]
            response_bytes += [bytes_in[1], bytes_in[2], bytes_in[3]]
            response_bytes += self.routine_control(bytes_in[1], identifier, option_record)
            return response_bytes

        if sid == ServiceId.security_access:
            if self.current == DiagnosticSessionType.default:
                raise UdsNegativeResponse(NegativeResponseCode.service_not_supported_in_active_session)
            if len(bytes_in) < 2:
                raise UdsNegativeResponse(NegativeResponseCode.incorrect_message_length_or_invalid_format)
            level = bytes_in[1]
            if level < 0x80:
                if level % 2 == 1:
                    response_bytes += self.security_access_request_seed(level)
                else:
                    if len(bytes_in) < 3:
                        raise UdsNegativeResponse(NegativeResponseCode.incorrect_message_length_or_invalid_format)
                    response_bytes += self.security_access_send_key(level, bytes_in[2:])
            else:
                if any(s.level == level for s in self.security_levels):
                    response_bytes += self.security_access_request_seed(level)
                elif any(s.level == level - 1 for s in self.security_levels):
                    if len(bytes_in) < 3:
                        raise UdsNegativeResponse(NegativeResponseCode.incorrect_message_length_or_invalid_format)
                    response_bytes += self.security_access_send_key(level, bytes_in[2:])
                else:
                    raise UdsNegativeResponse(NegativeResponseCode.request_out_of_range)
            return response_bytes

        if sid == ServiceId.tester_present:
            if len(bytes_in) == 1:
                response_bytes += self.tester_present(0x00)
                return response_bytes
            raise UdsNegativeResponse(NegativeResponseCode.sub_function_not_supported)

        if sid == ServiceId.transfer_data:
            if len(bytes_in) < 3:
                raise UdsNegativeResponse(NegativeResponseCode.incorrect_message_length_or_invalid_format)
            block_number = bytes_in[1]
            response_bytes += self.transfer_data(block_number, bytes_in[2:])
            return response_bytes

        if sid == ServiceId.write_data_by_identifier:
            if len(bytes_in) < 4:
                raise UdsNegativeResponse(NegativeResponseCode.incorrect_message_length_or_invalid_format)
            identifier = (bytes_in[1] << 8) | bytes_in[2]
            response_bytes += self.write_data_by(identifier, bytes_in[3:])
            return response_bytes

        if sid == ServiceId.write_memory_by_address:
            if len(bytes_in) < 5:
                raise UdsNegativeResponse(NegativeResponseCode.incorrect_message_length_or_invalid_format)
            format_spec = bytes_in[1]
            number_of_length_bytes = format_spec >> 4
            number_of_address_bytes = format_spec & 0x0F
            header_length = 2 + number_of_address_bytes + number_of_length_bytes
            if len(bytes_in) < header_length:
                raise UdsNegativeResponse(NegativeResponseCode.incorrect_message_length_or_invalid_format)
            address = bytes_in[2:2 + number_of_address_bytes]
            length = bytes_in[2 + number_of_address_bytes:header_length]
            data = bytes_in[header_length:]
            response_bytes += self.write_memory_by(address, length, data)
            return response_bytes

        if sid == ServiceId.kwp_read_ecu_identification:
            if len(bytes_in) != 2:
                raise UdsNegativeResponse(NegativeResponseCode.incorrect_message_length_or_invalid_format)
            identifier = bytes_in[1]
            response_bytes.append(identifier)
            response_bytes += self.read_ecu_identifier(identifier)
            return response_bytes

        if sid == ServiceId.kwp_read_data_by_local_identifier:
            if len(bytes_in) != 2:
                raise UdsNegativeResponse(NegativeResponseCode.incorrect_message_length_or_invalid_format)
            identifier = bytes_in[1]
            response_bytes.append(identifier)
            response_bytes += self.read_data_by_local_identifier(identifier)
            return response_bytes

        raise UdsNegativeResponse(NegativeResponseCode.service_not_supported)

    def perform_session_setup(self) -> None:
        super().perform_session_setup()
        data_table = self.ecu_identifiers.get(KWPConst.ecu_identification_data_table)
        scaling_table = self.ecu_identifiers.get(KWPConst.ecu_identification_scaling)
        if not data_table or not data_table.content:
            return
        if not scaling_table or not scaling_table.content:
            return
        try:
            entries = ECUIdentificationEntry.create_from(data_table.content, scaling_table.content)
        except Exception:
            return
        for entry in entries:
            identifier = entry.option
            name = f"KWP_ECU_{identifier}"
            self.ecu_identifiers[identifier] = Identifier(
                identifier=identifier,
                name=name,
                data_type=data_table.data_type,
                static_content=list(entry.data),
                value_type=("bytes", None),
                read_levels=[],
                write_levels=[],
            )

    def configure(self, property_name: str, value: str) -> None:
        if property_name == "vin":
            identifier = self.ecu_identifiers.get(KWPConst.ecu_identification_vin)
            if identifier:
                identifier.content = list(value.encode("utf-8"))
                self.ecu_identifiers[KWPConst.ecu_identification_vin] = identifier
            return
        super().configure(property_name, value)

    def read_data_by_local_identifier(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.local_identifiers:
            raise UdsNegativeResponse(NegativeResponseCode.request_out_of_range)
        identifier_obj = self.local_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_ecu_identifier(self, identifier: int) -> List[int]:
        self.reset_default_session_timer()
        identifier_obj = self.ecu_identifiers.get(identifier)
        if not identifier_obj:
            raise UdsNegativeResponse(NegativeResponseCode.request_out_of_range)
        content = identifier_obj.content
        if not content:
            return [0x00] * identifier_obj.data_type.length
        return content

    def read_diagnostic_trouble_codes_by_status(self) -> List[int]:
        dtc_bytes: List[int] = []
        number_of_dtcs = min(0xFF, len(self.dtcs))
        for dtc in self.dtcs:
            dtc_bytes.extend(dtc.bytes)
        return [number_of_dtcs] + dtc_bytes

    def read_dtc_by_status_mask(self, status_mask: int) -> List[int]:
        self.reset_default_session_timer()
        combined_mask = DTCStatusMask.none
        dtc_bytes: List[int] = []
        for dtc in self.dtcs:
            if dtc.state & DTCStatusMask(status_mask):
                dtc_bytes.extend(dtc.bytes)
                dtc_bytes.append(int(dtc.state))
                combined_mask |= dtc.state
        return [int(combined_mask)] + dtc_bytes
