# pylint: disable=fixme
# XXX: remove 'Provides: osbuild-dnf-json-api = 8' from the osbuild.spec file when this file is removed

import json
from typing import Any, Dict, TextIO

from osbuild.solver.exceptions import InvalidRequestError
from osbuild.solver.model import DepsolveResult, DumpResult, Package, Repository, SearchResult
from osbuild.solver.request import (
    DepsolveCmdArgs,
    DepsolveTransaction,
    SBOMRequest,
    SearchCmdArgs,
    SolverCommand,
    SolverConfig,
    SolverRequest,
)

from . import SolverAPIVersion


def _package_as_dict_dump_search(package: Package) -> dict:
    """
    Returns a dictionary representation of the RPM package for the DUMP and SEARCH commands.
    """
    return {
        "name": package.name,
        "summary": package.summary,
        "description": package.description,
        "url": package.url,
        "repo_id": package.repo_id,
        "epoch": package.epoch,
        "version": package.version,
        "release": package.release,
        "arch": package.arch,
        "buildtime": package.build_time_as_rfc3339(),
        "license": package.license,
    }


def _package_as_dict_depsolve(package: Package) -> dict:
    """
    Returns a dictionary representation of the RPM package for the DEPSOLVE command.
    """
    return {
        "name": package.name,
        "epoch": package.epoch,
        "version": package.version,
        "release": package.release,
        "arch": package.arch,
        "repo_id": package.repo_id,
        "path": package.location,
        "remote_location": package.remote_locations[0],
        "checksum": str(package.checksum),
    }


def _repository_as_dict(repository: Repository) -> dict:
    """
    Returns a dictionary representation of the repository. In v1, it is used only by the DEPSOLVE command.
    """
    return {
        "id": repository.repo_id,
        "name": repository.name,
        "baseurl": repository.baseurl,
        "metalink": repository.metalink,
        "mirrorlist": repository.mirrorlist,
        "gpgcheck": repository.gpgcheck,
        "repo_gpgcheck": repository.repo_gpgcheck,
        "gpgkeys": repository.gpgkey,
        "sslverify": repository.sslverify,
        "sslcacert": repository.sslcacert,
        "sslclientkey": repository.sslclientkey,
        "sslclientcert": repository.sslclientcert,
    }


# pylint: disable=unused-argument
def serialize_response_dump(solver: str, result: DumpResult, writer: TextIO) -> None:
    writer.write('[')
    first = True
    for package in result.packages:
        if not first:
            writer.write(', ')
        writer.write(json.dumps(_package_as_dict_dump_search(package)))
        first = False
    writer.write("]\n")


# pylint: disable=unused-argument
def serialize_response_search(solver: str, result: SearchResult, writer: TextIO) -> None:
    packages = [_package_as_dict_dump_search(package) for package in result.packages]
    writer.write(json.dumps(packages))
    writer.write("\n")


def serialize_response_depsolve(solver: str, result: DepsolveResult, writer: TextIO) -> None:
    last_transaction = result.transactions[-1] if result.transactions else []
    writer.write('{"solver": ')
    writer.write(json.dumps(solver))
    writer.write(', "packages": [')
    first = True
    for package in last_transaction:
        if not first:
            writer.write(', ')
        writer.write(json.dumps(_package_as_dict_depsolve(package)))
        first = False
    writer.write('], "repos": ')
    writer.write(json.dumps({repository.repo_id: _repository_as_dict(repository)
                 for repository in result.repositories}))
    writer.write(', "modules": ')
    writer.write(json.dumps(result.modules or {}))
    if result.sbom:
        writer.write(', "sbom": ')
        writer.write(json.dumps(result.sbom))
    writer.write('}\n')


# pylint: disable=too-many-branches
def _parse_repository(repo_dict: Dict[str, Any]) -> Repository:
    """
    Parse repository config from dict

    Note: Merges 'gpgkey' (singular) and 'gpgkeys' (plural) fields for backward compatibility.
    """

    if not isinstance(repo_dict, dict):
        raise TypeError("Repository config must be a dict")

    gpgkey = []
    # NB: 'gpgkey' is no longer used by osbuild/images implementation, remove it in the next API version.
    if "gpgkey" in repo_dict:
        gpgkey.append(repo_dict["gpgkey"])
    if "gpgkeys" in repo_dict:
        gpgkey.extend(repo_dict["gpgkeys"])

    kwargs = {
        "repo_id": repo_dict["id"],
    }
    if "name" in repo_dict:
        kwargs["name"] = repo_dict["name"]
    if "baseurl" in repo_dict:
        baseurl_value = repo_dict["baseurl"]
        if not isinstance(baseurl_value, list):
            raise TypeError(f"'baseurl' must be a list of URLs, got {type(baseurl_value).__name__}")
        kwargs["baseurl"] = baseurl_value
    if "metalink" in repo_dict:
        kwargs["metalink"] = repo_dict["metalink"]
    if "mirrorlist" in repo_dict:
        kwargs["mirrorlist"] = repo_dict["mirrorlist"]
    if "gpgcheck" in repo_dict:
        kwargs["gpgcheck"] = repo_dict["gpgcheck"]
    if "repo_gpgcheck" in repo_dict:
        kwargs["repo_gpgcheck"] = repo_dict["repo_gpgcheck"]
    if gpgkey:
        kwargs["gpgkey"] = gpgkey
    if "sslverify" in repo_dict:
        kwargs["sslverify"] = repo_dict["sslverify"]
    if "sslcacert" in repo_dict:
        kwargs["sslcacert"] = repo_dict["sslcacert"]
    if "sslclientkey" in repo_dict:
        kwargs["sslclientkey"] = repo_dict["sslclientkey"]
    if "sslclientcert" in repo_dict:
        kwargs["sslclientcert"] = repo_dict["sslclientcert"]
    if "metadata_expire" in repo_dict:
        kwargs["metadata_expire"] = repo_dict["metadata_expire"]
    if "module_hotfixes" in repo_dict:
        kwargs["module_hotfixes"] = repo_dict["module_hotfixes"]

    return Repository.from_request(**kwargs)


def _parse_depsolve_transaction(trans_dict: Dict[str, Any]) -> DepsolveTransaction:
    """Parse a transaction from a dict"""

    if not isinstance(trans_dict, dict):
        raise TypeError("Depsolve transaction must be a dict")

    kwargs = {
        "package_specs": trans_dict.get("package-specs", []),
    }
    if "exclude-specs" in trans_dict:
        kwargs["exclude_specs"] = trans_dict["exclude-specs"]
    if "repo-ids" in trans_dict:
        kwargs["repo_ids"] = trans_dict["repo-ids"]
    if "module-enable-specs" in trans_dict:
        kwargs["module_enable_specs"] = trans_dict["module-enable-specs"]
    if "install_weak_deps" in trans_dict:
        kwargs["install_weak_deps"] = trans_dict["install_weak_deps"]

    return DepsolveTransaction(**kwargs)


# pylint: disable=too-many-branches, too-many-statements
def parse_request(request_dict: Dict[str, Any]) -> SolverRequest:
    """Parse a V1 request dict into a SolverRequest object"""

    # Get required top-level fields
    try:
        command_str = request_dict["command"]
        arch = request_dict["arch"]
        releasever = request_dict["releasever"]
        cachedir = request_dict["cachedir"]
        arguments = request_dict["arguments"]
    except KeyError as e:
        raise InvalidRequestError(f"Missing required field {e}") from e

    # Validate and parse command
    try:
        command = SolverCommand(command_str)
    except ValueError as e:
        valid_cmds = ", ".join([c.value for c in SolverCommand])
        raise InvalidRequestError(
            f"Invalid command '{command_str}': must be one of {valid_cmds}"
        ) from e

    # Get optional top-level fields
    module_platform_id = request_dict.get("module_platform_id")
    proxy = request_dict.get("proxy")

    # Validate and parse command arguments
    if not isinstance(arguments, dict):
        raise InvalidRequestError("Field 'arguments' must be a dict")

    repos = None
    if "repos" in arguments:
        if not isinstance(arguments["repos"], list):
            raise InvalidRequestError("Field 'repos' must be a list")
        try:
            repos = [_parse_repository(r) for r in arguments["repos"]]
        except (ValueError, TypeError) as e:
            raise InvalidRequestError(f"Invalid repository config: {e}") from e
        except KeyError as e:
            raise InvalidRequestError(f"Missing required field {e} in 'repos' item configuration") from e

    root_dir = arguments.get("root_dir")
    optional_metadata = arguments.get("optional-metadata")

    if optional_metadata and not isinstance(optional_metadata, list):
        raise InvalidRequestError("Field 'optional-metadata' must be a list")

    depsolve_transactions = []
    if command == SolverCommand.DEPSOLVE and "transactions" in arguments:
        if not isinstance(arguments["transactions"], list):
            raise InvalidRequestError("Field 'transactions' must be a list")
        try:
            depsolve_transactions = [_parse_depsolve_transaction(t) for t in arguments["transactions"]]
        except (ValueError, TypeError) as e:
            raise InvalidRequestError(f"Invalid depsolve transaction: {e}") from e
        except KeyError as e:
            raise InvalidRequestError(f"Missing required field {e} in 'transactions' list") from e

    search_args = None
    if command == SolverCommand.SEARCH and "search" in arguments:
        if not isinstance(arguments["search"], dict):
            raise InvalidRequestError("Field 'search' must be a dict")
        try:
            packages = arguments["search"]["packages"]
            if not isinstance(packages, list):
                raise InvalidRequestError("Field 'packages' must be a list")
            latest = arguments["search"].get("latest", False)
            search_args = SearchCmdArgs(packages, latest)
        except (ValueError, TypeError) as e:
            raise InvalidRequestError(f"Invalid search arguments: {e}") from e
        except KeyError as e:
            raise InvalidRequestError(f"Missing required field {e} in 'search' dict") from e

    sbom_request = None
    if "sbom" in arguments:
        if not isinstance(arguments["sbom"], dict):
            raise InvalidRequestError("Field 'sbom' must be a dict")
        try:
            sbom_request = SBOMRequest(arguments["sbom"]["type"])
        except (ValueError, TypeError) as e:
            raise InvalidRequestError(f"Invalid value for 'type' in 'sbom': {e}") from e
        except KeyError as e:
            raise InvalidRequestError("Missing required field 'type' in 'sbom'") from e
        if command != SolverCommand.DEPSOLVE:
            raise InvalidRequestError("Field 'sbom' is only supported with 'depsolve' command")

    depsolve_args = None
    if depsolve_transactions:
        depsolve_args = DepsolveCmdArgs(
            transactions=depsolve_transactions,
            sbom_request=sbom_request,
        )

    return SolverRequest(
        api_version=SolverAPIVersion.V1,
        command=command,
        config=SolverConfig(
            arch=arch,
            releasever=releasever,
            cachedir=cachedir,
            module_platform_id=module_platform_id,
            proxy=proxy,
            repos=repos,
            root_dir=root_dir,
            optional_metadata=optional_metadata,
        ),
        depsolve_args=depsolve_args,
        search_args=search_args,
    )
