import datetime
import os
import socket
from pathlib import Path
from typing import Any
from typing import Dict
from typing import Optional
from typing import Union

from xsdata.formats.dataclass.parsers import DictDecoder

from ... import errors
from ..models.xsd.dataset import Dataset
from ..utils import constants
from . import xml
from .dataset_parameters.main import serialize_dataset_parameters


def serialize_dataset_message(
    beamline: str,
    proposal: str,
    dataset: str,
    path: Union[str, Path],
    metadata: Optional[Dict[str, Any]] = None,
    strict: bool = False,
    indent: Optional[str] = None,
) -> bytes:
    """
    :raises IcatMetadataSerializationError: If metadata serialization fails.
    :raises IcatMetadataValidationError: If ``strict=True`` and the metadata is invalid.
    :emits IcatMetadataValidationWarning: If ``strict=False`` and the metadata is invalid.
    :emits IcatMetadataDeprecatedWarning: If metadata is deprecated.
    """
    # Early raise missing root keys
    if not beamline:
        raise AssertionError("ICAT requires the beamline name")
    if not proposal:
        raise AssertionError("ICAT requires the proposal name")
    if not dataset:
        raise AssertionError("ICAT requires the dataset name")
    if not path:
        raise AssertionError("ICAT requires the dataset path")

    # Parameters
    if metadata:
        parameters = metadata.copy()
    else:
        parameters = {}

    # Required parameters
    if "Sample_name" not in parameters:
        raise AssertionError("ICAT metadata field 'Sample_name' is missing")

    # Default parameters
    now = datetime.datetime.now().astimezone()
    _ = parameters.setdefault("startDate", now)
    _ = parameters.setdefault("endDate", now)
    _ = parameters.setdefault("machine", socket.getfqdn())
    _ = parameters.setdefault("software", "pyicat-plus_v" + constants.release)
    _ = parameters.setdefault("complete", True)

    # Parameters that are provided as arguments
    _set_dataset_parameter(parameters, "datasetName", "dataset", dataset, strict)
    _set_dataset_parameter(
        parameters, "location", "path", path, strict, normalize=os.path.normpath
    )
    _set_dataset_parameter(parameters, "proposal", "proposal", proposal, strict)

    # Normalize parameters
    input_datasets = parameters.get("input_datasets", None)
    if input_datasets:
        parameters["input_datasets"] = [
            os.path.normpath(input_dataset) for input_dataset in input_datasets
        ]

    # XML message root
    root = {
        # Root element only
        "instrument": beamline,
        # Both root attribute and parameter value
        "complete": parameters["complete"],
        # Both root element and parameter value
        "investigation": parameters["proposal"],
        "name": parameters["datasetName"],
        "location": parameters["location"],
        "startDate": parameters["startDate"],
        "endDate": parameters["endDate"],
        # Both sample element and parameter value
        "sample": {
            "name": parameters["Sample_name"],
        },
    }

    # Serialize before XSD serialization
    root = xml.serialize_root(root)
    parameters = serialize_dataset_parameters(parameters, strict=strict)

    # Add parameters to root
    if parameters:
        root["parameter"] = []
    for name, value in parameters.items():
        root["parameter"].append({"name": name, "value": value})

    # Trick XSD validation (it currently does not validate values, only keys)
    root["sample"]["type"] = None

    # Serialize with XSD validation
    return raw_serialize_dataset_message(root, indent=indent)


def raw_serialize_dataset_message(
    data: Dict[str, Any], indent: Optional[str] = None
) -> bytes:
    """Serialize message with XSD 'dataset' schema."""
    model = DictDecoder().decode(data, Dataset)
    return xml.serialize_message(model, indent=indent)


def _set_dataset_parameter(
    parameters: Dict[str, Any],
    key: str,
    argument_name: str,
    argument_value: Any,
    strict: bool,
    normalize: Optional[callable] = None,
) -> None:
    """
    Normalize and set a dataset parameter to the value provided by a serialization argument.

    If the parameter is already set, overwrite it and emit a warning
    (or raise exception when strict=True) if they are not the same.
    """
    parameter_value = parameters.pop(key, None)

    if normalize:
        argument_value = normalize(argument_value)
        if parameter_value is not None:
            parameter_value = normalize(parameter_value)

    parameters[key] = argument_value

    if parameter_value is not None and parameter_value != argument_value:
        message = f"ICAT dataset metadata {key}={parameter_value} is not equal "
        f"to serialization argument {argument_name}={argument_value}. Use the argument value."
        errors.invalid_warn_or_raise(message, strict=strict)
