# Copyright (C) 2022 Microsoft Corporation.
#
# This file is part of cloud-init. See LICENSE file for license information.

import base64
import csv
import logging
import traceback
from datetime import datetime, timezone
from io import StringIO
from typing import Any, Dict, List, Optional, Tuple
from xml.etree import ElementTree as ET  # nosec B405

import requests

from cloudinit import subp, version
from cloudinit.url_helper import UrlError

LOG = logging.getLogger(__name__)

# Maximum supported password length for the Azure OSProfile on Linux.
# https://learn.microsoft.com/en-us/rest/api/compute/virtual-machines/create-or-update?view=rest-compute-2026-03-02&tabs=HTTP#osprofile
MAX_PASSWORD_LENGTH = 72


def encode_report(
    data: List[str], delimiter: str = "|", quotechar: str = "'"
) -> str:
    """Encode report data with csv."""
    with StringIO() as io:
        csv.writer(
            io,
            delimiter=delimiter,
            quotechar=quotechar,
            quoting=csv.QUOTE_MINIMAL,
        ).writerow(data)

        # strip trailing \r\n
        return io.getvalue().rstrip()


class ReportableError(Exception):
    def __init__(
        self, reason: str, *, supporting_data: Optional[Dict[str, Any]] = None
    ) -> None:
        self.agent = f"Cloud-Init/{version.version_string()}"
        self.documentation_url = "https://aka.ms/linuxprovisioningerror"
        self.reason = reason

        if supporting_data:
            self.supporting_data = supporting_data
        else:
            self.supporting_data = {}

        self.timestamp = datetime.now(timezone.utc)

    def as_encoded_report(
        self,
        *,
        vm_id: Optional[str],
    ) -> str:
        data = [
            "result=error",
            f"reason={self.reason}",
            f"agent={self.agent}",
        ]
        data += [f"{k}={v}" for k, v in self.supporting_data.items()]
        data += [
            f"vm_id={vm_id}",
            f"timestamp={self.timestamp.isoformat()}",
            f"documentation_url={self.documentation_url}",
        ]

        return encode_report(data)

    def __eq__(self, other) -> bool:
        return (
            isinstance(other, ReportableError)
            and self.timestamp == other.timestamp
            and self.reason == other.reason
            and self.supporting_data == other.supporting_data
        )

    def __repr__(self) -> str:
        return (
            f"{self.__class__.__name__}("
            f"reason={self.reason}, "
            f"timestamp={self.timestamp}, "
            f"supporting_data={self.supporting_data})"
        )


class ReportableErrorDhcpInterfaceNotFound(ReportableError):
    def __init__(self, duration: float) -> None:
        super().__init__("failure to find DHCP interface")

        self.supporting_data["duration"] = duration


class ReportableErrorDhcpLease(ReportableError):
    def __init__(
        self,
        driver: Optional[str],
        duration: float,
        interface: Optional[str],
        mac_address: Optional[str],
    ) -> None:
        super().__init__("failure to obtain DHCP lease")

        self.supporting_data["driver"] = driver
        self.supporting_data["duration"] = duration
        self.supporting_data["interface"] = interface
        self.supporting_data["mac_address"] = mac_address


class ReportableErrorDhcpOnNonPrimaryInterface(ReportableError):
    def __init__(
        self,
        *,
        interface: Optional[str],
        driver: Optional[str],
        router: Optional[str],
        static_routes: Optional[List[Tuple[str, str]]],
        lease: Dict[str, Any],
    ) -> None:
        super().__init__("failure to find primary DHCP interface")

        self.supporting_data["interface"] = interface
        self.supporting_data["driver"] = driver
        self.supporting_data["router"] = router
        self.supporting_data["static_routes"] = static_routes
        self.supporting_data["lease"] = lease


class ReportableErrorImdsUrlError(ReportableError):
    def __init__(self, *, exception: UrlError, duration: float) -> None:
        # ConnectTimeout sub-classes ConnectError so order is important.
        if isinstance(exception.cause, requests.ConnectTimeout):
            reason = "connection timeout querying IMDS"
        elif isinstance(exception.cause, requests.ConnectionError):
            reason = "connection error querying IMDS"
        elif isinstance(exception.cause, requests.ReadTimeout):
            reason = "read timeout querying IMDS"
        elif exception.code:
            reason = f"http error {exception.code} querying IMDS"
        else:
            reason = "unexpected error querying IMDS"

        super().__init__(reason)

        if exception.code:
            self.supporting_data["http_code"] = exception.code

        self.supporting_data["duration"] = duration
        self.supporting_data["exception"] = repr(exception)
        self.supporting_data["url"] = exception.url


class ReportableErrorImdsInvalidMetadata(ReportableError):
    def __init__(self, *, key: str, value: Any) -> None:
        super().__init__(f"invalid IMDS metadata for key={key}")

        self.supporting_data["key"] = key
        self.supporting_data["value"] = value
        self.supporting_data["type"] = type(value).__name__


class ReportableErrorMissingCustomData(ReportableError):
    def __init__(
        self,
        *,
        pps_type: str,
        provisioning_media: str,
    ) -> None:
        super().__init__("failure to read customData while hasCustomData=true")

        self.supporting_data["pps_type"] = pps_type
        self.supporting_data["provisioning_media"] = provisioning_media


class ReportableErrorImdsMetadataParsingException(ReportableError):
    def __init__(self, *, exception: ValueError) -> None:
        super().__init__("error parsing IMDS metadata")

        self.supporting_data["exception"] = repr(exception)


class ReportableErrorImportError(ReportableError):
    def __init__(self, *, error: ImportError) -> None:
        super().__init__(f"error importing {error.name} library")

        self.supporting_data["error"] = repr(error)


class ReportableErrorOsDiskPpsFailure(ReportableError):
    def __init__(self) -> None:
        super().__init__("error waiting for host shutdown")


class ReportableErrorOvfInvalidMetadata(ReportableError):
    def __init__(self, message: str) -> None:
        super().__init__(f"unexpected metadata parsing ovf-env.xml: {message}")


class ReportableErrorOvfParsingException(ReportableError):
    def __init__(self, *, exception: ET.ParseError) -> None:
        message = exception.msg
        super().__init__(f"error parsing ovf-env.xml: {message}")


class ReportableErrorOsProfilePasswordTooLong(ReportableError):
    def __init__(
        self, *, length: int, max_length: int = MAX_PASSWORD_LENGTH
    ) -> None:
        super().__init__(
            f"unsupported password length={length} max={max_length}"
        )

        self.supporting_data["length"] = length
        self.supporting_data["max_length"] = max_length


class ReportableErrorUnhandledException(ReportableError):
    def __init__(self, exception: Exception) -> None:
        super().__init__("unhandled exception")

        trace = "".join(
            traceback.format_exception(
                type(exception), exception, exception.__traceback__
            )
        )
        trace_lines = trace.split("\n")
        reversed_trace_lines = trace_lines[::-1]
        reversed_trace = "\n".join(reversed_trace_lines)
        trace_base64 = base64.b64encode(reversed_trace.encode("utf-8")).decode(
            "utf-8"
        )

        self.supporting_data["exception"] = repr(exception)
        self.supporting_data["traceback_base64"] = trace_base64


class ReportableErrorProxyAgentNotFound(ReportableError):
    def __init__(self) -> None:
        super().__init__("azure-proxy-agent not found")


class ReportableErrorProxyAgentStatusFailure(ReportableError):
    def __init__(self, exception: subp.ProcessExecutionError) -> None:
        super().__init__("azure-proxy-agent status failure")

        self.supporting_data["exit_code"] = exception.exit_code
        self.supporting_data["stdout"] = exception.stdout
        self.supporting_data["stderr"] = exception.stderr


class ReportableErrorVmIdentification(ReportableError):
    def __init__(
        self, *, exception: Exception, system_uuid: Optional[str] = None
    ) -> None:
        super().__init__("failure to identify Azure VM ID")

        self.supporting_data["exception"] = repr(exception)
        self.supporting_data["system_uuid"] = system_uuid
