from __future__ import annotations

import json
import logging
import os
from dataclasses import dataclass
from typing import Any, Dict, List, Optional

from .helpers import (
    SpecParseError,
    parse_float,
    parse_hex_array,
    parse_hex_bytes,
    parse_hex_bytes_optional,
    parse_hex_int,
    parse_hex_string_list,
    parse_optional_string,
)
from ..core.actions import Action, ActionKind, HostAction, LockStrategy
from ..core.automotive import AutomotiveDTC, DTCStatusMask, DTCStatusMaskValue, Identifier, MemoryBlock, SecurityLevel
from ..core.obd2.frame import Frame
from ..core.obd2.generators import (
    ConstantValueGenerator,
    CosineWaveGenerator,
    CurveGenerator,
    SawtoothWaveGenerator,
    SineWaveGenerator,
    TriangleWaveGenerator,
)
from ..core.obd2.pid import DynamicPIDValue, PID, PIDEncoding, PIDRegistry, SequencePIDValue
from ..core.routine import Routine, RoutineResponse
from ..core.service import MatchType, Replay, Service
from ..core.sessions.obd2 import OBD2DiagnosticSession
from ..core.sessions.uds import UDSDiagnosticSession
from ..core.sessions.kwp import KWPDiagnosticSession
from ..core.uds import DataType

logger = logging.getLogger("ecumulator.specs")


@dataclass
class VehicleSpec:
    vin: str
    ecus: List[Dict[str, Any]]


class ValueFactory:
    @staticmethod
    def make_dynamic_value(identifier_label: str, spec: Dict) -> Optional[DynamicPIDValue]:
        function = spec.get("function")
        period = spec.get("period", 0)
        if function != "constant" and period <= 0:
            logger.debug("%s uses function '%s' with non-positive period %s", identifier_label, function, period)
            return None
        generator = ValueFactory.make_generator(spec, identifier_label)
        encoding = ValueFactory.make_encoding(spec.get("encoding") or {}, identifier_label)
        if generator is None or encoding is None:
            return None
        return DynamicPIDValue(generator=generator, encoding=encoding)

    @staticmethod
    def make_sequence_value(identifier_label: str, spec) -> Optional[SequencePIDValue]:
        responses = None
        repeat_mode = "loop"
        if isinstance(spec, list):
            responses = parse_hex_string_list(spec)
        elif isinstance(spec, dict):
            repeat_mode = spec.get("repeat", "loop")
            responses = parse_hex_string_list(spec.get("responses", []))
        if not responses:
            logger.debug("%s defines empty sequence", identifier_label)
            return None
        length = len(responses[0])
        if length == 0 or any(len(resp) != length for resp in responses):
            logger.debug("%s defines invalid sequence lengths", identifier_label)
            return None
        behavior = "loop" if repeat_mode == "loop" else "once"
        return SequencePIDValue(responses=responses, behavior=behavior)

    @staticmethod
    def make_generator(spec: Dict, identifier_label: str):
        function = spec.get("function")
        minimum = float(spec.get("min", 0.0))
        maximum = float(spec.get("max", 0.0))
        period = float(spec.get("period", 0.0))
        offset = float(spec.get("offset", 0.0))
        if function == "sine":
            return SineWaveGenerator(minimum, maximum, period, offset)
        if function == "cosine":
            return CosineWaveGenerator(minimum, maximum, period, offset)
        if function == "sawtooth":
            return SawtoothWaveGenerator(minimum, maximum, period, offset)
        if function == "triangle":
            return TriangleWaveGenerator(minimum, maximum, period, offset)
        if function == "constant":
            return ConstantValueGenerator(minimum)
        if function == "curve":
            nodes = spec.get("nodes", [])
            interpolation = spec.get("interpolation", "smooth")
            loop_spec = spec.get("loop", {})
            loop_start = float(loop_spec.get("startTime", 0.0))
            loop_end = float(loop_spec.get("endTime", 1.0))
            loop_mode = loop_spec.get("mode", "forward")
            return CurveGenerator(
                minimum, maximum, period, offset, nodes, interpolation,
                loop_start, loop_end, loop_mode
            )
        logger.debug("%s references unknown dynamic function '%s'", identifier_label, function)
        return None

    @staticmethod
    def make_encoding(spec: Dict, identifier_label: str) -> Optional[PIDEncoding]:
        enc_type = spec.get("type")
        scale = float(spec.get("scale", 1.0))
        offset = float(spec.get("offset", 0.0))
        if enc_type in ("uint8", "uint16", "int8", "int16"):
            return PIDEncoding(kind=enc_type, scale=scale, offset=offset)
        logger.debug("%s references unknown encoding type '%s'", identifier_label, enc_type)
        return None


def load_vehicle(path: str) -> VehicleSpec:
    with open(path, "r", encoding="utf-8") as handle:
        data = json.load(handle)
    return VehicleSpec(vin=data.get("vin", ""), ecus=data.get("ecus", []))


def load_command_handler(path: str):
    with open(path, "r", encoding="utf-8") as handle:
        data = json.load(handle)
    base_dir = os.path.dirname(os.path.realpath(path))
    return parse_command_handler(data, base_dir)


def parse_command_handler(data: Dict, base_dir: str):
    kind = data.get("kind", "custom")
    if kind == "obd2":
        return parse_obd2(data["obd2"])
    if kind == "uds":
        return parse_uds(data["uds"], base_dir)
    if kind == "kwp":
        return parse_kwp(data["kwp"], base_dir)
    raise SpecParseError(f"Unsupported handler kind '{kind}'")


def parse_arbitration(info: Dict) -> Dict:
    return {
        "request": parse_hex_int(info["request"]),
        "reply": parse_hex_int(info["reply"]),
        "extended_request": parse_hex_int(info["extendedRequest"]) if info.get("extendedRequest") is not None else None,
        "extended_reply": parse_hex_int(info["extendedReply"]) if info.get("extendedReply") is not None else None,
        "auxiliary": [parse_hex_int(item) for item in info.get("auxiliary", [])],
        "auxiliary_request_extensions": [parse_hex_int(item) for item in info.get("auxiliaryRequestExtensions", [])],
    }


def parse_dtcs(dtcs: List[Dict]) -> List[AutomotiveDTC]:
    result = []
    for dtc in dtcs:
        bytes_out = parse_hex_array(dtc["bytes"])
        state = DTCStatusMask.none
        for status in dtc.get("status", []):
            try:
                mask_value = DTCStatusMaskValue(status)
            except ValueError:
                continue
            state |= mask_value.status_mask
        try:
            result.append(AutomotiveDTC(bytes=bytes_out, state=state))
        except Exception:
            continue
    return result


def parse_actions(actions: List[Dict]) -> List[Action]:
    parsed = []
    for action in actions:
        kind = action.get("kind", "unknown")
        if kind == "host":
            strategy = action.get("strategy", "")
            try:
                host = HostAction(strategy)
            except ValueError:
                continue
            parsed.append(Action.host(host))
        elif kind == "setArbitration":
            what = action.get("what", {})
            request = what.get("request")
            reply = what.get("reply")
            aux = what.get("auxiliary", [])
            if not request or not reply:
                continue
            request_id = parse_hex_int(request)
            reply_id = parse_hex_int(reply)
            aux_ids = [parse_hex_int(item) for item in aux]
            parsed.append(Action.set_arbitration(request_id, reply_id, aux_ids))
        elif kind == "lockAllOtherECUs":
            strategy = action.get("strategy", "")
            lock = LockStrategy.bmw_nrc_forbidden if strategy == "bmwNrcForbidden" else LockStrategy.default
            parsed.append(Action.lock_all_other_ecus(lock))
        elif kind == "overrideReplyId":
            what = action.get("what", {})
            reply = what.get("reply")
            if not reply:
                continue
            parsed.append(Action.override_reply_id(parse_hex_int(reply)))
        elif kind == "unlockAllOtherECUs":
            parsed.append(Action.unlock_all_other_ecus())
    return parsed


def parse_security_levels(levels: List[Dict]) -> List[SecurityLevel]:
    result = []
    for level in levels:
        seed_key_pairs = []
        for pair in level.get("seedKeyPairs", []):
            seed = parse_hex_bytes(pair.get("seed"))
            key = parse_hex_bytes(pair.get("key"))
            seed_key_pairs.append(SecurityLevel.SeedKeyPair(seed, key))
        actions = parse_actions(level.get("actions", []))
        result.append(SecurityLevel(level=parse_hex_int(level["level"]), seed_key_pairs=seed_key_pairs, delay=int(level.get("delay", 0)), actions=actions))
    return result


def parse_routines(routines: List[Dict]) -> List[Routine]:
    result = []
    for routine in routines:
        responses: List[RoutineResponse] = []
        for response in routine.get("responses", []):
            responses.append(
                RoutineResponse(
                    option_record=parse_hex_array(response.get("optionRecord", [])),
                    delay=int(response.get("delay", 0)),
                    start_response=parse_hex_array(response.get("startResponse", [])),
                    stop_response=parse_hex_bytes_optional(response.get("stopResponse")),
                    results_response=parse_hex_bytes_optional(response.get("resultsResponse")),
                )
            )
        actions = parse_actions(routine.get("actions", []))
        result.append(
            Routine(
                name=routine.get("name", "Unknown Routine"),
                identifier=parse_hex_int(routine["id"]),
                responses=responses,
                security_levels=routine.get("securityLevels", []),
                actions=actions,
                debug_swallow_response_likeliness=float(routine.get("debugSwallowResponseLikeliness", 0.0)),
                ignore_option_record=bool(routine.get("ignoreOptionRecord", False)),
            )
        )
    return result


def parse_services(services: List[Dict]) -> List[Service]:
    result = []
    for service in services:
        match_type = MatchType(service.get("matchType", "unknown")) if service.get("matchType") else MatchType.unknown
        replay = None
        if service.get("replay"):
            replay = [Replay(count=item.get("count", 0), response=parse_hex_array(item.get("response", []))) for item in service.get("replay", [])]
        actions = parse_actions(service.get("actions", []))
        result.append(
            Service(
                name=service.get("name", ""),
                request=parse_hex_bytes(service.get("request")),
                match_type=match_type,
                delay=int(service.get("delay", 0)),
                response=parse_hex_bytes_optional(service.get("response")),
                replay=replay,
                replay_loop=bool(service.get("replayLoop", False)),
                actions=actions,
                debug_swallow_response_likeliness=float(service.get("debugSwallowResponseLikeliness", 0.0)),
            )
        )
    return result


def parse_identifiers(identifiers: List[Dict]) -> List[Identifier]:
    parsed = []
    for identifier in identifiers:
        identifier_id = parse_hex_int(identifier["id"])
        data_type = DataType(identifier.get("type", "unspecified")) if identifier.get("type") else DataType.unspecified
        name = identifier.get("name") or f"Unknown:{identifier_id}"
        bytes_value = parse_hex_bytes_optional(identifier.get("bytes"))
        content = identifier.get("content")
        static_bytes: List[int] = []
        if content is not None:
            static_bytes = list(str(content).encode("utf-8"))
        elif bytes_value is not None:
            static_bytes = bytes_value
        read_levels = identifier.get("readLevels", [])
        write_levels = identifier.get("writeLevels", [])

        value_type = ("bytes", None)
        dynamic_spec = identifier.get("dynamic")
        sequence_spec = identifier.get("sequence")
        label = f"Identifier 0x{identifier_id:04X}"
        if dynamic_spec:
            dynamic_value = ValueFactory.make_dynamic_value(label, dynamic_spec)
            if dynamic_value:
                value_type = ("dynamic", dynamic_value)
        if value_type[0] == "bytes" and sequence_spec:
            sequence_value = ValueFactory.make_sequence_value(label, sequence_spec)
            if sequence_value:
                value_type = ("sequence", sequence_value)
        parsed.append(
            Identifier(
                identifier=identifier_id,
                name=name,
                data_type=data_type,
                static_content=static_bytes,
                value_type=value_type,
                read_levels=read_levels,
                write_levels=write_levels,
            )
        )
    return parsed


def parse_memory_blocks(blocks: List[Dict], base_dir: str) -> List[MemoryBlock]:
    parsed = []
    for block in blocks:
        name = block.get("name", "")
        identifier = parse_hex_int(block.get("id"))
        address_bytes = parse_hex_bytes(block.get("address"), allow_odd_length=True)
        size_bytes = parse_hex_bytes(block.get("size"), allow_odd_length=True)
        address = int.from_bytes(bytes(address_bytes), "big")
        size = int.from_bytes(bytes(size_bytes), "big")
        mtu = parse_hex_int(block.get("mtu"))
        def resolve_path(path: Optional[str]) -> Optional[str]:
            if not path:
                return None
            if os.path.isabs(path):
                return path
            return os.path.join(base_dir, path)
        parsed.append(
            MemoryBlock(
                name=name,
                identifier=identifier,
                address=address,
                size=size,
                mtu=mtu,
                file_to_load=resolve_path(block.get("fileToLoad")),
                file_to_compare_with=resolve_path(block.get("fileToCompareWith")),
                file_to_store=resolve_path(block.get("fileToStore")),
                skip_erase_check=bool(block.get("skipEraseCheck", False)),
                skip_size_check=bool(block.get("skipSizeCheck", False)),
                debug_swallow_response_likeliness=float(block.get("debugSwallowResponseLikeliness", 0.0)),
            )
        )
    return parsed


def parse_frames(frames: List[Dict]) -> List[Frame]:
    parsed = []
    for frame in frames:
        pids = PIDRegistry()
        for pid_spec in frame.get("pids", []):
            pid_id = parse_hex_int(pid_spec["id"])
            simple = pid_spec.get("simple")
            dynamic_spec = pid_spec.get("dynamic")
            sequence_spec = pid_spec.get("sequence")
            if simple is not None:
                simple_bytes = None
                if isinstance(simple, list):
                    simple_bytes = [parse_hex_int(item) for item in simple]
                else:
                    simple_bytes = parse_hex_bytes(simple)
                pid = PID(("static", simple_bytes))
            elif dynamic_spec is not None:
                dynamic_value = ValueFactory.make_dynamic_value(f"PID 0x{pid_id:02X}", dynamic_spec)
                pid = PID(("dynamic", dynamic_value)) if dynamic_value else PID(("static", []))
            elif sequence_spec is not None:
                sequence_value = ValueFactory.make_sequence_value(f"PID 0x{pid_id:02X}", sequence_spec)
                pid = PID(("sequence", sequence_value)) if sequence_value else PID(("static", []))
            else:
                pid = PID(("static", []))
            pids[pid_id] = pid
        parsed.append(Frame(pids=pids))
    return parsed


def parse_obd2(spec: Dict) -> OBD2DiagnosticSession:
    arbitration = parse_arbitration(spec["arbitrationInfo"])
    frames = parse_frames(spec.get("frames", []))
    informational = {parse_hex_int(pid["id"]): parse_pid(pid) for pid in spec.get("informational", [])}
    oxygen = {parse_hex_int(pid["id"]): parse_pid(pid) for pid in spec.get("oxygenSensorMonitoring", [])}
    on_board = {parse_hex_int(pid["id"]): parse_pid(pid) for pid in spec.get("onBoardMonitoring", [])}
    control_operations = {parse_hex_int(pid["id"]): parse_pid(pid) for pid in spec.get("controlOperations", [])}
    dtcs = parse_dtcs(spec.get("dtcs", []))
    stored_dtcs = [dtc for dtc in dtcs if dtc.state & DTCStatusMask.confirmed_dtc]
    pending_dtcs = [dtc for dtc in dtcs if dtc.state & DTCStatusMask.pending_dtc]
    permanent_spec = spec.get("permanentDtcs")
    if permanent_spec is not None:
        permanent_dtcs = parse_dtcs(permanent_spec)
    else:
        permanent_dtcs = [dtc for dtc in dtcs if (dtc.state & DTCStatusMask.confirmed_dtc and dtc.state & DTCStatusMask.pending_dtc)]
    return OBD2DiagnosticSession(
        arbitration_id=arbitration["request"],
        reply_id=arbitration["reply"],
        auxiliary_ids=arbitration["auxiliary"],
        vin=parse_optional_string(spec.get("vin")),
        frames=frames,
        stored_dtcs=stored_dtcs,
        pending_dtcs=pending_dtcs,
        permanent_dtcs=permanent_dtcs,
        informational=informational,
        oxygen_sensor_monitoring=oxygen,
        on_board_monitoring=on_board,
        control_operations=control_operations,
    )


def parse_pid(pid_spec: Dict) -> PID:
    pid_id = parse_hex_int(pid_spec["id"])
    simple = pid_spec.get("simple")
    dynamic_spec = pid_spec.get("dynamic")
    sequence_spec = pid_spec.get("sequence")
    if simple is not None:
        if isinstance(simple, list):
            simple_bytes = [parse_hex_int(item) for item in simple]
        else:
            simple_bytes = parse_hex_bytes(simple)
        return PID(("static", simple_bytes))
    if dynamic_spec is not None:
        dynamic_value = ValueFactory.make_dynamic_value(f"PID 0x{pid_id:02X}", dynamic_spec)
        return PID(("dynamic", dynamic_value)) if dynamic_value else PID(("static", []))
    if sequence_spec is not None:
        sequence_value = ValueFactory.make_sequence_value(f"PID 0x{pid_id:02X}", sequence_spec)
        return PID(("sequence", sequence_value)) if sequence_value else PID(("static", []))
    return PID(("static", []))


def parse_uds(spec: Dict, base_dir: str) -> UDSDiagnosticSession:
    arbitration = parse_arbitration(spec["arbitration"])
    identifiers = parse_identifiers(spec.get("identifiers", []))
    routines = parse_routines(spec.get("routines", []))
    security_levels = parse_security_levels(spec.get("securityLevels", []))
    memory_layout = parse_memory_blocks(spec.get("memoryLayout", []), base_dir)
    custom_services = parse_services(spec.get("customServices", []))
    dtcs = parse_dtcs(spec.get("dtcs", []))
    startup_actions = parse_actions(spec.get("startupActions", []))
    
    # Parse ISOTP configuration
    isotp_config = spec.get("isotp", {})
    isotp_block_size = int(isotp_config.get("blockSize", 0x20))  # Default: 32 frames
    isotp_separation_time = int(isotp_config.get("separationTimeMs", 0))  # Default: 0ms
    
    session = UDSDiagnosticSession(
        arbitration_id=arbitration["request"],
        reply_id=arbitration["reply"],
        auxiliary_ids=arbitration["auxiliary"],
        request_extension=arbitration.get("extended_request"),
        reply_extension=arbitration.get("extended_reply"),
        auxiliary_request_extensions=arbitration.get("auxiliary_request_extensions", []),
        p2_server_max=parse_float(spec.get("p2ServerMax"), 0.05),
        p2ext_server_max=parse_float(spec.get("p2extServerMax"), 2.0),
        s3_session_timeout=parse_float(spec.get("s3SessionTimeout"), 5.0),
        memory=memory_layout,
        ecu_identifiers=[],
        local_identifiers=[],
        identifiers=identifiers,
        routines=routines,
        security_levels=security_levels,
        custom_services=custom_services,
        dtcs=dtcs,
        dtcs_clearing_delay=parse_float(spec.get("dtcsClearingDelay"), 0.0),
        startup_actions=startup_actions,
    )
    
    # Store ISOTP configuration on the session
    session.isotp_block_size = isotp_block_size
    session.isotp_separation_time = isotp_separation_time
    
    return session


def parse_kwp(spec: Dict, base_dir: str) -> KWPDiagnosticSession:
    arbitration = parse_arbitration(spec["arbitration"])
    identifiers = parse_identifiers(spec.get("commonIdentifiers", []))
    ecu_identifiers = parse_identifiers(spec.get("ecuIdentifiers", []))
    local_identifiers = parse_identifiers(spec.get("localIdentifiers", []))
    routines = parse_routines(spec.get("routines", []))
    security_levels = parse_security_levels(spec.get("securityLevels", []))
    memory_layout = parse_memory_blocks(spec.get("memoryLayout", []), base_dir)
    custom_services = parse_services(spec.get("customServices", []))
    dtcs = parse_dtcs(spec.get("dtcs", []))
    startup_actions = parse_actions(spec.get("startupActions", []))
    timing_parameters = parse_hex_array(spec.get("timingParameters", []))
    
    # Parse ISOTP configuration
    isotp_config = spec.get("isotp", {})
    isotp_block_size = int(isotp_config.get("blockSize", 0x20))  # Default: 32 frames
    isotp_separation_time = int(isotp_config.get("separationTimeMs", 0))  # Default: 0ms
    
    session = KWPDiagnosticSession(
        arbitration_id=arbitration["request"],
        reply_id=arbitration["reply"],
        auxiliary_ids=arbitration["auxiliary"],
        request_extension=arbitration.get("extended_request"),
        reply_extension=arbitration.get("extended_reply"),
        auxiliary_request_extensions=arbitration.get("auxiliary_request_extensions", []),
        p2_server_max=parse_float(spec.get("p2ServerMax"), 0.05),
        p2ext_server_max=parse_float(spec.get("p2extServerMax"), 2.0),
        s3_session_timeout=parse_float(spec.get("s3SessionTimeout"), 5.0),
        memory=memory_layout,
        ecu_identifiers=ecu_identifiers,
        local_identifiers=local_identifiers,
        identifiers=identifiers,
        routines=routines,
        security_levels=security_levels,
        custom_services=custom_services,
        dtcs=dtcs,
        dtcs_clearing_delay=parse_float(spec.get("dtcsClearingDelay"), 0.0),
        startup_actions=startup_actions,
        kwp_timing_parameters=timing_parameters,
    )
    
    # Store ISOTP configuration on the session
    session.isotp_block_size = isotp_block_size
    session.isotp_separation_time = isotp_separation_time
    
    return session
