# Copyright © The Debusine Developers
# See the AUTHORS file at the top-level directory of this distribution
#
# This file is part of Debusine. It is subject to the license terms
# in the LICENSE file found in the top-level directory of this
# distribution. No part of Debusine, including this file, may be copied,
# modified, propagated, or distributed except according to the terms
# contained in the LICENSE file.

"""Debusine command line interface, artifact management commands."""

import argparse
import os
from pathlib import Path
from typing import Any, override

from dateutil.parser import isoparse
from requests.exceptions import RequestException

from debusine.artifacts import LocalArtifact
from debusine.artifacts.local_artifact import NewLocalFile
from debusine.client.client_utils import (
    get_debian_package,
    prepare_changes_for_upload,
    prepare_deb_for_upload,
    prepare_dsc_for_upload,
)
from debusine.client.commands.base import (
    DebusineCommand,
    OptionalInputDataCommand,
    WorkspaceCommand,
)
from debusine.client.models import RelationType


class Create(OptionalInputDataCommand, WorkspaceCommand, group="artifact"):
    """Create an artifact."""

    @override
    @classmethod
    def configure(cls, parser: argparse.ArgumentParser) -> None:
        super().configure(parser)
        parser.add_argument(
            "category",
            type=str,
            help="Category of artifact",
            choices=list(map(str, LocalArtifact.artifact_categories())),
        )
        parser.add_argument(
            "--upload", nargs="+", help="Files to upload", default=[]
        )
        parser.add_argument(
            "--upload-base-directory",
            type=str,
            default=None,
            help="Base directory for files with relative file path",
        )
        parser.add_argument(
            "--expire-at",
            type=isoparse,
            help="If not passed: artifact does not expire. "
            "If passed: set expire date of artifact.",
        )

    @override
    def run(self) -> None:
        workspace = self.workspace
        expire_at = self.args.expire_at

        artifact_cls = LocalArtifact.class_from_category(self.args.category)

        if self.args.upload_base_directory is not None:
            upload_base_dir = Path(self.args.upload_base_directory).absolute()
        else:
            upload_base_dir = None

        try:
            local_artifact = artifact_cls._create(
                data=self.input_data or {},
                files=[
                    NewLocalFile(
                        file=Path(upload_path),
                        artifact_base_dir=upload_base_dir,
                    )
                    for upload_path in self.args.upload
                ],
            )
        except ValueError as exc:
            self._fail(f"Cannot create artifact: {exc}")

        with self._api_call_or_fail():
            artifact_created = self.debusine.artifact_create(
                local_artifact, workspace=workspace, expire_at=expire_at
            )

        output: dict[str, Any]
        output = {
            "result": "success",
            "message": f"New artifact created: {artifact_created.url}",
            "artifact_id": artifact_created.id,
            "files_to_upload": len(artifact_created.files_to_upload),
            "expire_at": expire_at,
        }

        files_to_upload = {}
        for artifact_path in artifact_created.files_to_upload:
            files_to_upload[artifact_path] = local_artifact.files[artifact_path]

        self.print_yaml_output(output)
        with self._api_call_or_fail():
            self.debusine.upload_files(artifact_created.id, files_to_upload)


class LegacyCreate(
    Create, name="create-artifact", deprecated="see `artifact create`"
):
    """Create an artifact."""


class Show(DebusineCommand, group="artifact"):
    """Show artifact information."""

    @override
    @classmethod
    def configure(cls, parser: argparse.ArgumentParser) -> None:
        super().configure(parser)
        parser.add_argument(
            "id", type=int, help="Show information of the artifact"
        )

    @override
    def run(self) -> None:
        with self._api_call_or_fail():
            artifact = self.debusine.artifact_get(self.args.id)
        self.print_yaml_output(artifact.model_dump(mode="json"))


class LegacyShow(Show, name="show-artifact", deprecated="see `artifact show`"):
    """Show artifact information."""


class ShowRelations(DebusineCommand, group="artifact", name="show-relations"):
    """Show artifact relations."""

    @override
    @classmethod
    def configure(cls, parser: argparse.ArgumentParser) -> None:
        super().configure(parser)
        parser.add_argument(
            "id", type=int, help="Show the relations of an artifact"
        )
        parser.add_argument(
            "--reverse",
            action="store_true",
            help="Show reverse relationships, rather than forward ones",
        )

    @override
    def run(self) -> None:
        artifact_id = self.args.id
        with self._api_call_or_fail():
            if self.args.reverse:
                models = self.debusine.relation_list(target_id=artifact_id)
            else:
                models = self.debusine.relation_list(artifact_id=artifact_id)

        relations = [model.model_dump(mode="json") for model in models]
        self.print_yaml_output(relations)


class LegacyShowRelations(
    ShowRelations,
    name="show-artifact-relations",
    deprecated="see `artifact show-relations`",
):
    """Show artifact relations."""


class ImportDebian(WorkspaceCommand, group="artifact", name="import-debian"):
    """Import a Debian source/binary package."""

    @override
    @classmethod
    def configure(cls, parser: argparse.ArgumentParser) -> None:
        super().configure(parser)
        parser.add_argument(
            "--expire-at",
            type=isoparse,
            help="If not passed: artifact does not expire. "
            "If passed: set expire date of artifact.",
        )
        parser.add_argument(
            "upload",
            help=".changes, .dsc, or .deb to upload",
        )

    @override
    def run(self) -> None:
        workspace = self.workspace
        upload = self.args.upload
        expire_at = self.args.expire_at
        result: dict[str, Any] = {}
        uploaded = []
        try:
            with get_debian_package(upload) as path:
                artifacts: list[LocalArtifact[Any]]
                if path.suffix == ".changes":
                    artifacts = prepare_changes_for_upload(path)
                elif path.suffix == ".dsc":
                    artifacts = [prepare_dsc_for_upload(path)]
                elif path.suffix in (".deb", ".ddeb", ".udeb"):
                    artifacts = [prepare_deb_for_upload(path)]
                else:
                    raise ValueError(
                        "Only source packages (.dsc), binary packages (.deb), "
                        "and source/binary uploads (.changes) can be directly "
                        f"imported with this command. {path} is not supported."
                    )
                for artifact in artifacts:
                    with self._api_call_or_fail():
                        remote_artifact = self.debusine.upload_artifact(
                            artifact, workspace=workspace, expire_at=expire_at
                        )
                    uploaded.append(remote_artifact)
            primary_artifact = uploaded[0]
            result = {
                "result": "success",
                "message": f"New artifact created: {primary_artifact.url}",
                "artifact_id": primary_artifact.id,
            }
            for related_artifact in uploaded[1:]:
                with self._api_call_or_fail():
                    self.debusine.relation_create(
                        primary_artifact.id,
                        related_artifact.id,
                        RelationType.EXTENDS,
                    )
                result.setdefault("extends", []).append(
                    {"artifact_id": related_artifact.id}
                )
                with self._api_call_or_fail():
                    self.debusine.relation_create(
                        primary_artifact.id,
                        related_artifact.id,
                        RelationType.RELATES_TO,
                    )
                result.setdefault("relates_to", []).append(
                    {"artifact_id": related_artifact.id}
                )
        except (
            FileNotFoundError,
            ValueError,
            RequestException,
        ) as err:
            self.show_error(err)

        self.print_yaml_output(result)


class LegacyImportDebian(
    ImportDebian,
    name="import-debian-artifact",
    deprecated="see `artifact import-debian`",
):
    """Import a Debian source/binary package."""


class Download(DebusineCommand, group="artifact"):
    """Download an artifact in .tar.gz format."""

    @override
    @classmethod
    def configure(cls, parser: argparse.ArgumentParser) -> None:
        super().configure(parser)
        parser.add_argument("id", type=int, help="Artifact to download")
        parser.add_argument(
            "--target-directory",
            type=Path,
            help="Directory to save the artifact",
            default=Path.cwd(),
        )
        parser.add_argument(
            "--tarball",
            action="store_true",
            help="Save the artifact as .tar.gz",
        )

    @override
    def run(self) -> None:
        target_directory = self.args.target_directory
        if not os.access(target_directory, os.W_OK):
            self._fail(f"Error: Cannot write to {target_directory}")

        with self._api_call_or_fail():
            self.debusine.download_artifact(
                self.args.id,
                destination=target_directory,
                tarball=self.args.tarball,
            )


class LegacyDownload(
    Download,
    name="download-artifact",
    deprecated="see `artifact download`",
):
    """Download an artifact in .tar.gz format."""
