"""Methods for managing the artifact's state."""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

from ._remote_methods import ArtifactMethod
from ._utils import (
    check_errors,
    get_headers,
    get_method_url,
    params_commit,
    params_create,
    params_delete,
    params_edit,
    prepare_params,
)

if TYPE_CHECKING:
    from . import AsyncHyphaArtifact


async def edit(
    self: AsyncHyphaArtifact,
    manifest: dict[str, Any] | None = None,
    type: str | None = None,  # noqa: A002
    config: dict[str, Any] | None = None,
    secrets: dict[str, str] | None = None,
    version: str | None = None,
    comment: str | None = None,
    *,
    stage: bool = False,
) -> None:
    """Edits an existing artifact's manifest.

    Parameters
    ----------
    self: AsyncHyphaArtifact
        The HyphaArtifact instance to use.

    artifact_id: The id of the artifact to edit. It can be an uuid generated by
        create or edit function, or it can be an alias of the artifact under the
        current workspace. If you want to refer to an artifact in another
        workspace, you should use the full alias in the format of
        "workspace_id/alias".

    manifest: The updated manifest. Ensure the manifest follows the required schema
        if applicable (e.g., for collections).

    type: Optional. The type of the artifact. Supported values are collection,
        generic and any other custom type. By default, it's set to generic which
        contains fields tailored for displaying the artifact as cards on a webpage.

    permissions: Optional.
        A dictionary containing user permissions.
        For example {"*": "r+"} gives read and create access to everyone,
        {"@": "rw+"} allows all authenticated users to read/write/create,
        {"user_id_1": "r+"} grants read and create permissions to a specific user.
        You can also set permissions for specific operations,
        such as {"user_id_1": ["read", "create"]}. See detailed explanation about
        permissions below.

    secrets: Optional. A dictionary containing secrets to be stored with the
        artifact.
        Secrets are encrypted and can only be accessed by the artifact owner
        or users with appropriate permissions. See the create function for a
        list of supported secrets.

    config: Optional. A dictionary containing additional configuration options for
        the artifact.

    version: Optional. Strict Validation Applied: Must be None, "new", or an
        existing version name from the artifact's versions array. Custom version
        names are NOT allowed during edit - they can only be specified during
        commit().

        None (default): Updates the latest existing version
        "new": Creates a new version immediately (unless stage=True)
        "stage": Enters staging mode (equivalent to stage=True)
        "existing_version_name": Updates the specified existing version
            (must already exist in the versions array)

    Special Staging Behavior: When stage=True and version="new" are used together,
        it stores an intent marker for creating a new version during commit,
        and allows custom version names to be specified later during the commit()
        operation.

    stage: Optional. If True, the artifact will be edited in staging mode regardless
        of the version parameter. Default is False. When in staging mode, the
        artifact must be committed to finalize changes.

    comment: Optional.
        A comment to describe the changes made to the artifact.

    Important Notes:

        Version Validation: The system now strictly validates the version parameter.
            You cannot specify arbitrary custom version names during edit.
        Staging Intent System: When you use version="new" with stage=True,
            the system stores an "intent" to create a new version, which allows you
            to specify a custom version name later during commit().
        Historical Version Editing: You can edit specific existing versions by providing
            their exact version name (e.g., version="v1"), but the version must already
            exist.

    """
    edit_params = params_edit(
        manifest=manifest,
        type=type,
        config=config,
        secrets=secrets,
        version=version,
        comment=comment,
        stage=stage,
    )
    params: dict[str, Any] = prepare_params(
        self,
        edit_params,
    )

    url = get_method_url(self, ArtifactMethod.EDIT)

    response = await self.get_client().post(
        url,
        headers=get_headers(self),
        json=params,
    )

    check_errors(response)


async def commit(
    self: AsyncHyphaArtifact,
    version: str | None = None,
    comment: str | None = None,
) -> None:
    """Commit staged changes to an artifact.

    Parameters
    ----------
    self: AsyncHyphaArtifact
        The HyphaArtifact instance to use.
    artifact_id: str.
        The id of the artifact to commit. Can be a UUID or alias.
    version: Optional.
        Custom version name. If not provided, a sequential version name
        will be generated.
    comment: Optional.
        Comment describing the changes in this version.

    Returns
    -------
    A dictionary containing the committed artifact with updated versions and no
    staging data.

    Behavior:

        Version Creation: Only creates a new version if version="new" was explicitly
            specified during edit operations
        Version Update: If no new version intent exists, updates the existing
            latest version (or creates v0 if no versions exist)
        Metadata-Only Operation: Commit is now a fast, metadata-only operation with
        no file copying
        File Placement:
            Files are already in their final locations (placed there during upload
            based on intent)
        Staging Cleanup: The staging area is cleared and metadata is finalized

    Performance Note: The commit operation is highly optimized and performs no file
    copying, making it fast even for artifacts with large files.

    """
    commit_params = params_commit(
        version=version,
        comment=comment,
    )
    # Param building handled centrally in utilities.
    params: dict[str, Any] = prepare_params(
        self,
        commit_params,
    )

    response = await self.get_client().post(
        get_method_url(self, ArtifactMethod.COMMIT),
        headers=get_headers(self),
        json=params,
    )

    check_errors(response)


async def discard(
    self: AsyncHyphaArtifact,
) -> None:
    """Discards all staged changes for an artifact.

    Parameters
    ----------
    self: AsyncHyphaArtifact
        The HyphaArtifact instance to use.

    Returns
    -------
    A dictionary containing the artifact reverted to its last committed state.

    Behavior:

        Clears all staged manifest changes, reverting to the last committed manifest
        Removes all staged files from S3 storage
        Clears the staging area (sets staging to None)
        Preserves all committed versions and their files
        Raises an error if no staged changes exist

    """
    response = await self.get_client().post(
        get_method_url(self, ArtifactMethod.DISCARD),
        headers=get_headers(self),
        json=prepare_params(self),
    )

    check_errors(response)


async def create(
    self: AsyncHyphaArtifact,
    manifest: str | dict[str, Any] | None = None,
    parent_id: str | None = None,
    config: dict[str, Any] | None = None,
    version: str | None = None,
    comment: str | None = None,
    secrets: dict[str, str] | None = None,
    *,
    type: str | None = None,  # noqa: A002
    overwrite: bool | None = None,
    stage: bool | None = None,
) -> None:
    """Create a new artifact or collection with the specified manifest.

    The artifact is staged until committed. For collections, the collection field should
    be an empty list.

    Parameters
    ----------
    self: AsyncHyphaArtifact
        The HyphaArtifact instance to use.

    workspace: Optional. The workspace id where the artifact will be created.
        If not set, it will be created in the default workspace. If specified,
        it should match the workspace in the artifact_id and also the parent_id.

    parent_id: The id of the parent collection where the artifact will be created.
        If the artifact is a top-level collection,
        leave this field empty or set to None.

    type: The type of the artifact. Supported values are collection,
        vector-collection, generic and any other custom type. By default, it's set to
    generic which contains fields tailored for displaying the artifact as cards
    on a webpage.

    manifest: The manifest of the new artifact. Ensure the manifest follows the
        required schema if applicable (e.g., for collections).

    config: Optional. A dictionary containing additional configuration options for
        the artifact (shared for both staged and committed). For collections,
        the config can contain the following special fields:

        collection_schema: Optional. A JSON schema that defines the structure of
            child artifacts in the collection. This schema is used to validate
            child artifacts when they are created or edited. If a child artifact
            does not conform to the schema, the creation or edit operation will fail
        id_parts: Optional. A dictionary of id name parts to be used in generating
            the id for child artifacts. For example:
            {"animals": ["dog", "cat", ...], "colors": ["red", "blue", ...]}.
            This can be used for creating child artifacts with auto-generated ids
            based on the id parts. For example, when calling create, you can specify
            the alias as my-pet-{colors}-{animals}, and the id will be generated
            based on the id parts, e.g., my-pet-red-dog.
        permissions: Optional. A dictionary containing user permissions. For example
            {"*": "r+"} gives read and create access to everyone, {"@": "rw+"}
            allows all authenticated users to read/write/create, and {"user_id_1": "r+"}
            grants read and create permissions to a specific user. You can also set
            permissions for specific operations, such as
            {"user_id_1": ["read", "create"]}. See detailed explanation about
            permissions below.
    list_fields: Optional. A list of fields to be collected when calling list
        function. By default, it collects all fields in the artifacts. If you want
        to collect only specific fields, you can set this field to a list of field
        names, e.g. ["manifest", "download_count"].
        publish_to: Optional. A string specifying the target platform to publish the
        artifact. Supported values are zenodo and sandbox_zenodo. If set, the artifact
        will be published to the specified platform. The artifact must have a valid
        Zenodo metadata schema to be published.

    version: Optional. Version Creation Behavior: Controls initial version creation
        for the artifact.

        None or "new" (default): Creates version "v0" immediately (unless stage=True)
        "stage": Creates the artifact in staging mode (equivalent to stage=True)

    Important: If stage=True is specified, any version parameter is ignored and
        the artifact starts in staging mode. When a staged artifact is committed,
        it will create version "v0" by default.

    stage: Optional. If True, the artifact will be created in staging mode regardless
        of the version parameter. Default is False. When in staging mode,
        you must call commit() to finalize the artifact with its first version.

    comment: Optional.
        A comment to describe the changes made to the artifact.

    secrets: Optional.
        A dictionary containing secrets to be stored with the artifact.
        Secrets are encrypted and can only be accessed by the artifact owner or users
        with appropriate permissions. The following keys can be used:

        ZENODO_ACCESS_TOKEN: The Zenodo access token to publish the artifact to Zenodo.
        SANDBOX_ZENODO_ACCESS_TOKEN: The Zenodo access token to publish the artifact to
            the Zenodo sandbox.
        S3_ENDPOINT_URL: The endpoint URL of the S3 storage for the artifact.
        S3_ACCESS_KEY_ID: The access key ID for the S3 storage for the artifact.
        S3_SECRET_ACCESS_KEY: The secret access key for the S3 storage for the artifact.
        S3_REGION_NAME: The region name of the S3 storage for the artifact.
        S3_BUCKET: The bucket name of the S3 storage for the artifact. Default to the
            hypha workspaces bucket.
        S3_PREFIX: The prefix of the S3 storage for the artifact. Default: "".
    S3_PUBLIC_ENDPOINT_URL: The public endpoint URL of the S3 storage for the
    artifact. If the S3 server is not public, you can set this to the public
    endpoint URL. Default: None.

    overwrite: Optional.
        A boolean flag to overwrite the existing artifact with the
        same alias. Default is False.

    Note 1: If you set stage=True, you must call commit() to finalize the artifact.

    """
    if manifest is None:
        manifest = {
            "name": self.artifact_alias,
            "description": "Artifact created using package hypha-artifact.",
        }

    # For CREATE, the server API does not accept an 'artifact_id' parameter.
    # Build params without using prepare_params to avoid injecting it.
    # Normalize parent_id: server accepts alias within the current workspace.
    normalized_parent: str | None
    if parent_id and "/" in parent_id:
        normalized_parent = parent_id.split("/", 1)[1]
    else:
        normalized_parent = parent_id

    create_params = params_create(
        alias=self.artifact_alias,
        workspace=self.workspace,
        parent_id=normalized_parent,
        artifact_type=type,
        manifest=manifest,
        config=config,
        version=version,
        stage=stage,
        comment=comment,
        secrets=secrets,
        overwrite=overwrite,
    )
    # Note: create() must not include artifact_id; send create_params directly.
    params: dict[str, Any] = dict(create_params)

    response = await self.get_client().post(
        get_method_url(self, ArtifactMethod.CREATE),
        headers=get_headers(self),
        json=params,
    )

    check_errors(response)


async def delete(
    self: AsyncHyphaArtifact,
    version: str | None = None,
    *,
    delete_files: bool | None = None,
    recursive: bool | None = None,
) -> None:
    """Delete an artifact, its manifest, and all associated files.

    Parameters
    ----------
    self: AsyncHyphaArtifact
        The HyphaArtifact instance to use.
    artifact_id: str
        The id of the artifact to delete. It can be an uuid generated by
        create or edit function, or it can be an alias of the artifact under the current
        workspace. If you want to refer to an artifact in another workspace, you should
        use the full alias in the format of "workspace_id/alias".
    delete_files: Optional.
        A boolean flag to delete all files associated with the
    artifact. Default is False.
    recursive: Optional. A boolean flag to delete all child artifacts recursively.
        Default is False.
    version: Optional. Specifies which version to delete:
        None (default): Delete the entire artifact and all its versions
        "stage": Error - Cannot delete staged version. Use discard() instead to
        remove staged changes
        "latest": Delete the latest committed version
        "v1", "v2", etc.: Delete a specific version by name (must exist in version
        history)
        Integer index: Delete version by index (0-based)

    Version Handling:

        Staged Artifacts: If you try to delete a staged version (version="stage"),
            the system will raise an error and direct you to use the discard()
            function instead, which is the proper way to remove staged changes.
        Version Validation: When deleting a specific version, the system validates
            that the version exists in the artifact's version history. If the version
            doesn't exist, an error is raised showing available versions.
        Complete Deletion: When version=None, the entire artifact is deleted including
            all versions and metadata.

    Warning: If delete_files is set to True, recursive must be set to True, all
    child artifacts will be deleted, and all files associated with the child
    artifacts will be permanently deleted from the S3 storage. This operation
    is irreversible.

    """
    delete_params = params_delete(
        delete_files=delete_files,
        recursive=recursive,
        version=version,
    )
    params: dict[str, Any] = prepare_params(self, delete_params)

    response = await self.get_client().post(
        get_method_url(self, ArtifactMethod.DELETE),
        headers=get_headers(self),
        json=params,
    )

    check_errors(response)


async def list_children(
    self: AsyncHyphaArtifact,
    keywords: list[str] | None = None,
    filters: dict[str, Any] | None = None,
    mode: str = "AND",
    offset: int = 0,
    limit: int = 100,
    order_by: str | None = None,
    *,
    silent: bool = False,
    stage: bool = False,
) -> dict[str, Any]:
    """Retrieve a list of child artifacts within a specified collection.

    Supports keyword-based fuzzy search, field-specific filters, and flexible ordering.
    This function allows detailed control over the search and pagination of
    artifacts in a collection, including staged artifacts if specified.

    Parameters
    ----------
    self: AsyncHyphaArtifact
        The instance of the AsyncHyphaArtifact class.

    parent_id: str
        The id of the parent artifact or collection to list children
        from. It can be an uuid generated by create or edit function, or it can be
        an alias of the artifact under the current workspace. If you want to refer
        to an artifact in another workspace, you should use the full alias in the
        format of "workspace_id/alias". If not specified, the function lists all
        top-level artifacts in the current workspace.

    keywords: List[str], optional
        A list of search terms used for fuzzy searching
        across all manifest fields. Each term is searched independently, and
        results matching any term will be included. For example,
        ["sample", "dataset"] returns artifacts containing either
        "sample" or "dataset" in any field of the manifest.

    filters: dict, optional
        A dictionary where each key is a manifest field name
        and each value specifies the match for that field. Filters support both
        exact and range-based matching, depending on the field. You can filter
        based on the keys inside the manifest, as well as internal fields like
        permissions and view/download statistics. Supported internal
        fields include:

        type: Matches the artifact type exactly, e.g., {"type": "application"}.
        created_by: Matches the exact creator ID, e.g., {"created_by": "user123"}.
        created_at and last_modified: Accept a single timestamp (lower bound) or
            a range for filtering. For example,
            {"created_at": [1620000000, 1630000000]} filters artifacts created
            between the two timestamps.
        view_count and download_count: Accept a single value or a range for
            filtering, as with date fields. For example, {"view_count": [10, 100]}
            filters artifacts viewed between 10 and 100 times.
        permissions/<user_id>: Searches for artifacts with specific permissions
            assigned to the given user_id.
        version: Matches the exact version of the artifact, it only support
            "stage", "committed" or "*" (both staged or committed). If stage is
            specified, this filter should align with the stage parameter.
        manifest: Matches the exact value of the field, e.g.,
            "manifest": {"name": "example-dataset"}. These filters also support
            fuzzy matching if a value contains a wildcard (*),
            e.g., "manifest": {"name": "dataset*"}, depending on the SQL backend.
        config: Matches the exact value of the field in the config, e.g.,
            "config": {"collection_schema": {"type": "object"}}.

    mode: str, optional
        Defines how multiple conditions (from keywords and filters) are combined.
        Use "AND" to ensure all conditions must match, or "OR" to includ
        artifacts meeting any condition.
        Default is "AND".

    offset: int, optional
        The number of artifacts to skip before listing results.
        Default is 0.

    limit: int, optional
        The maximum number of artifacts to return.
        Default is 100.

    order_by: str, optional
        The field used to order results. Options include:
        Built-in fields: view_count, download_count, last_modified, created_at, and id.
        Custom JSON fields: manifest.<field_name> or config.<field_name>
            (e.g., manifest.likes, config.priority).
        Use a suffix < or > to specify ascending or descending order, respectively
            (e.g., view_count< for ascending, manifest.likes> for descending).
        Default ordering is ascending by id if not specified.

    silent: bool, optional
        If True, prevents incrementing the view count for the parent artifact when
        listing children. Default is False.

    stage: bool, optional
        Controls which artifacts to return based on their staging status:
        True: Return only staged artifacts
        False: Return only committed artifacts (default)
        'all': Return both staged and committed artifacts

    Returns: A list of artifacts that match the search criteria, each represented by
        a dictionary containing all the fields.

    """
    params: dict[str, Any] = {
        k: v
        for k, v in {
            "parent_id": self.artifact_id,
            "keywords": keywords,
            "filters": filters,
            "mode": mode,
            "offset": offset,
            "limit": limit,
            "order_by": order_by,
            "silent": silent,
            "stage": True if stage else None,
        }.items()
        if v is not None
    }

    response = await self.get_client().post(
        get_method_url(self, ArtifactMethod.LIST),
        headers=get_headers(self),
        json=params,
    )
    # Raise for server-side errors and normalize return payload
    check_errors(response)
    return response.json()
