"""Response Parser for IPP."""
from __future__ import annotations

import logging
import struct
from datetime import datetime, timedelta, timezone
from typing import Any

from .enums import ATTRIBUTE_ENUM_MAP, IppTag
from .exceptions import IPPParseError

_LOGGER = logging.getLogger(__name__)


def parse_ieee1284_device_id(device_id: str) -> dict[str, str]:
    """Parse IEEE 1284 device id for common device info."""
    if not device_id:
        return {}

    device_id = device_id.strip(";")
    device_info: dict[str, str] = {}

    for pair in device_id.split(";"):
        key, value = pair.split(":", 2)
        device_info[key.strip()] = value.strip()

    if not device_info.get("MANUFACTURER") and device_info.get("MFG"):
        device_info["MANUFACTURER"] = device_info["MFG"]

    if not device_info.get("MODEL") and device_info.get("MDL"):
        device_info["MODEL"] = device_info["MDL"]

    if not device_info.get("COMMAND SET") and device_info.get("CMD"):
        device_info["COMMAND SET"] = device_info["CMD"]

    return device_info


def parse_collection(data: bytes, offset: int) -> tuple[dict[str, Any], int]:
    """Parse member attributes from IPP collection."""
    collection_data: dict[str, Any] = {}
    member_name: str = ""

    _LOGGER.debug("#BeginCollectionOffset: %s", offset)

    while struct.unpack_from("b", data, offset)[0] != IppTag.END_COLLECTION.value:
        attribute, next_attr_offset = parse_attribute(data, offset, member_name)

        if attribute["tag"] == IppTag.MEMBER_NAME.value:
            member_name = attribute["value"]
        elif member_name:
            collection_data[member_name] = attribute["value"]

        offset = next_attr_offset

    _LOGGER.debug("#EndCollectionOffset: %s", offset)

    # skip over end of collection marker
    _, next_attr_offset = parse_attribute(data, offset)

    return collection_data, next_attr_offset


# pylint: disable=R0912,R0915
def parse_attribute(  # noqa: PLR0912, PLR0915
    data: bytes,
    offset: int,
    prev_attr_name: str = "",
) -> tuple[dict[str, Any], int]:
    """Parse attribute from IPP data.

    1 byte: Tag - b
    2 byte: Name Length - h
    N bytes: Name - direct access
    2 byte: Value Length -h
    N bytes: Value - direct access
    """
    _LOGGER.debug("Parsing Attribute at offset %s", offset)

    attribute = {"tag": struct.unpack_from(">b", data, offset)[0]}
    offset += 1

    attribute["name-length"] = struct.unpack_from(">h", data, offset)[0]
    offset += 2

    offset_length = offset + attribute["name-length"]
    attribute["name"] = data[offset:offset_length].decode("utf-8")
    offset += attribute["name-length"]

    resolved_attr_name = attribute["name"] or prev_attr_name

    if attribute["name"]:
        _LOGGER.debug(
            "Attribute Name: %s (%s)", attribute["name"], hex(attribute["tag"]),
        )
    else:
        _LOGGER.debug("Attribute Tag: %s", hex(attribute["tag"]))

    attribute["value-length"] = struct.unpack_from(">h", data, offset)[0]
    offset += 2

    _LOGGER.debug("Attribute Value Offset: %s", offset)
    _LOGGER.debug("Attribute Value Length: %s", attribute["value-length"])

    if attribute["tag"] in (IppTag.ENUM.value, IppTag.INTEGER.value):
        attribute["value"] = struct.unpack_from(">i", data, offset)[0]

        if (
            attribute["tag"] == IppTag.ENUM.value
            and resolved_attr_name in ATTRIBUTE_ENUM_MAP
        ):
            enum_class = ATTRIBUTE_ENUM_MAP[resolved_attr_name]
            attribute["value"] = enum_class(attribute["value"])

        offset += 4
        _LOGGER.debug("Attribute Value: %s", attribute["value"])
    elif attribute["tag"] == IppTag.BOOLEAN.value:
        attribute["value"] = struct.unpack_from(">?", data, offset)[0]
        offset += 1
        _LOGGER.debug("Attribute Value: %s", attribute["value"])
    elif attribute["tag"] == IppTag.DATE.value:
        if attribute["value-length"] != 11:
            raise IPPParseError(
                f'Invalid DATE size {attribute["value-length"]}',  # noqa: EM102
            )

        raw_date = dict(
            zip(
                (
                    "year",
                    "month",
                    "day",
                    "hour",
                    "minute",
                    "second",
                    "decisecond",
                    "tz_dir",
                    "tz_hour",
                    "tz_minute",
                ),
                struct.unpack_from(">hbbbbbbcbb", data, offset),
            ),
        )
        raw_date["microsecond"] = raw_date.pop("decisecond") * 100_000
        raw_date["tzinfo"] = timezone(
            {b"+": 1, b"-": -1}[raw_date.pop("tz_dir")]
            * timedelta(
                hours=raw_date.pop("tz_hour"),
                minutes=raw_date.pop("tz_minute"),
            ),
        )

        attribute["value"] = datetime(**raw_date)  # noqa: DTZ001
        offset += attribute["value-length"]
        _LOGGER.debug("Attribute Value: %s", attribute["value"])
    elif attribute["tag"] == IppTag.RESERVED_STRING.value:
        if attribute["value-length"] > 0:
            offset_length = offset + attribute["value-length"]
            attribute["value"] = data[offset:offset_length].decode("utf-8")
            offset += attribute["value-length"]
        else:
            attribute["value"] = None

        _LOGGER.debug("Attribute Value: %s", attribute["value"])
    elif attribute["tag"] == IppTag.RANGE.value:
        attribute["value"] = []
        for i in range(int(attribute["value-length"] / 4)):
            attribute["value"].append(struct.unpack_from(">i", data, offset + i * 4)[0])
        offset += attribute["value-length"]
        _LOGGER.debug("Attribute Value: %s", attribute["value"])
    elif attribute["tag"] == IppTag.RESOLUTION.value:
        attribute["value"] = struct.unpack_from(">iib", data, offset)
        offset += attribute["value-length"]
    elif attribute["tag"] in (IppTag.TEXT_LANG.value, IppTag.NAME_LANG.value):
        attribute["language-length"] = struct.unpack_from(">h", data, offset)[0]
        offset += 2

        offset_length = offset + attribute["language-length"]
        attribute["language"] = data[offset:offset_length].decode("utf-8")
        offset += attribute["language-length"]
        _LOGGER.debug("Attribute Language: %s", attribute["language"])

        attribute["text-length"] = struct.unpack_from(">h", data, offset)[0]
        offset += 2
        _LOGGER.debug("Attribute Text Length: %s", attribute["text-length"])

        offset_length = offset + attribute["text-length"]
        attribute["value"] = data[offset:offset_length].decode("utf-8")
        offset += attribute["text-length"]
        _LOGGER.debug("Attribute Value: %s", attribute["value"])
    elif attribute["tag"] == IppTag.BEGIN_COLLECTION.value:
        offset += attribute["value-length"]
        collection, new_offset = parse_collection(data, offset)
        attribute["value"] = collection
        offset = new_offset
        _LOGGER.debug("Attribute Value: %s", attribute["value"])
    elif attribute["tag"] == IppTag.END_COLLECTION.value:
        offset += attribute["value-length"]
    else:
        offset_length = offset + attribute["value-length"]
        attribute["value"] = data[offset:offset_length]
        _LOGGER.debug("Attribute Bytes: %s", attribute["value"])

        attribute["value"] = attribute["value"].decode("utf-8", "ignore")
        offset += attribute["value-length"]
        _LOGGER.debug("Attribute Value: %s", attribute["value"])

    return attribute, offset


# pylint: disable=R0912,R0915
def parse(  # noqa: PLR0912, PLR0915
    raw_data: bytes,
    contains_data: bool = False,  # noqa: FBT001, FBT002
) -> dict[str, Any]:
    r"""Parse raw IPP data.

    1 byte: Protocol Major Version - b
    1 byte: Protocol Minor Version - b
    2 byte: Operation ID/Status Code - h
    4 byte: Request ID - i

    1 byte: Operation Attribute Byte (\0x01)

    N Mal: Attributes

    1 byte: Attribute End Byte (\0x03)
    """
    data: dict[str, Any] = {}
    offset = 0

    _LOGGER.debug("Parsing IPP Data")

    data["version"] = struct.unpack_from(">bb", raw_data, offset)
    offset += 2

    _LOGGER.debug("IPP Version: %s", data["version"])

    data["status-code"] = struct.unpack_from(">h", raw_data, offset)[0]
    offset += 2

    _LOGGER.debug("IPP Status Code: %s", data["status-code"])

    data["request-id"] = struct.unpack_from(">i", raw_data, offset)[0]
    offset += 4

    data["operation-attributes"] = []
    data["unsupported-attributes"] = []
    data["jobs"] = []
    data["printers"] = []
    data["data"] = b""

    attribute_key = ""
    previous_attribute_name = ""
    tmp_data: dict[str, Any] = {}

    while struct.unpack_from("b", raw_data, offset)[0] != IppTag.END.value:
        # check for operation, job or printer attribute start byte
        # if tmp data and attribute key is set, another operation was sent
        # add it and reset tmp data
        if struct.unpack_from("b", raw_data, offset)[0] == IppTag.OPERATION.value:
            if tmp_data and attribute_key:
                data[attribute_key].append(tmp_data)
                tmp_data = {}

            attribute_key = "operation-attributes"
            offset += 1
        elif struct.unpack_from("b", raw_data, offset)[0] == IppTag.JOB.value:
            if tmp_data and attribute_key:
                data[attribute_key].append(tmp_data)
                tmp_data = {}

            attribute_key = "jobs"
            offset += 1
        elif struct.unpack_from("b", raw_data, offset)[0] == IppTag.PRINTER.value:
            if tmp_data and attribute_key:
                data[attribute_key].append(tmp_data)
                tmp_data = {}

            attribute_key = "printers"
            offset += 1
        elif (
            struct.unpack_from("b", raw_data, offset)[0]
            == IppTag.UNSUPPORTED_GROUP.value
        ):
            if tmp_data and attribute_key:
                data[attribute_key].append(tmp_data)
                tmp_data = {}

            attribute_key = "unsupported-attributes"
            offset += 1

        attribute, new_offset = parse_attribute(
            raw_data, offset, previous_attribute_name,
        )

        # if attribute has a name -> add it
        # if attribute doesn't have a name -> it is part of an array
        if attribute["name"]:
            tmp_data[attribute["name"]] = attribute["value"]
            previous_attribute_name = attribute["name"]
        elif previous_attribute_name:
            # check if attribute is already an array
            # else convert it to an array
            if isinstance(tmp_data[previous_attribute_name], list):
                tmp_data[previous_attribute_name].append(attribute["value"])
            else:
                tmp_value = tmp_data[previous_attribute_name]
                tmp_data[previous_attribute_name] = [tmp_value, attribute["value"]]

        offset = new_offset

    if isinstance(data[attribute_key], list):
        data[attribute_key].append(tmp_data)

    if isinstance(data["operation-attributes"], list):
        data["operation-attributes"] = data["operation-attributes"][0]

    if contains_data:
        offset_start = offset + 1
        data["data"] = raw_data[offset_start:]

    return data


def parse_make_and_model(make_and_model: str) -> tuple[str, str]:
    """Parse make and model for separate device make and model."""
    if not (make_and_model := make_and_model.strip()):
        return ("Unknown", "Unknown")

    make = "Unknown"
    model = "Unknown"
    found_make = False
    known_makes = [
        "brother",
        "canon",
        "epson",
        "kyocera",
        "hp",
        "xerox",
    ]

    test_against = make_and_model.lower()
    for known_make in known_makes:
        if test_against.startswith(known_make):
            found_make = True
            mlen = len(known_make)
            make = make_and_model[:mlen]
            model = make_and_model[mlen:].strip()
            break

    if not found_make:
        split = make_and_model.split(None, 1)
        make = split[0]

        if len(split) == 2:
            model = split[1].strip()

    return (make, model)
