# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.

"""
All the `deadline manifest` commands:
    * snapshot
    * upload
    * diff
    * download
"""

from __future__ import annotations

from configparser import ConfigParser
import dataclasses
import os
import sys
from typing import List, Optional
import boto3
import click

from deadline.client import api
from deadline.client.config import config_file
from deadline.job_attachments._diff import pretty_print_cli
from deadline.job_attachments._utils import (
    WINDOWS_MAX_PATH_LENGTH,
    _is_windows_long_path_registry_enabled,
)
from deadline.job_attachments.api.manifest import (
    _glob_files,
    _manifest_diff,
    _manifest_download,
    _manifest_snapshot,
    _manifest_upload,
)
from deadline.job_attachments.models import (
    S3_MANIFEST_FOLDER_NAME,
    JobAttachmentS3Settings,
    ManifestDiff,
    AssetType,
)

from ...exceptions import NonValidInputError
from .._common import _apply_cli_options_to_config, _handle_error
from .._main import deadline as main
from .click_logger import ClickLogger


@main.group(name="manifest")
@_handle_error
def cli_manifest():
    """
    BETA - Commands to work with [Deadline Cloud job attachments].

    [Deadline Cloud job attachments]: https://docs.aws.amazon.com/deadline-cloud/latest/userguide/storage-job-attachments.html
    """


@cli_manifest.command(name="snapshot")
@click.option("--root", required=True, help="The root directory to snapshot. ")
@click.option(
    "-d",
    "--destination",
    default=None,
    help="Destination directory where manifest is created. Defaults to the manifest root directory.",
)
@click.option(
    "-n",
    "--name",
    default=None,
    help="Name of the manifest. A timestamp is added YYYY-MM-DD-HH-MM-SS for versioning.",
)
@click.option(
    "-i",
    "--include",
    default=None,
    help="Glob syntax of files and directories to include in the manifest. Can be provided multiple times.",
    multiple=True,
)
@click.option(
    "-e",
    "--exclude",
    default=None,
    help="Glob syntax of files and directories to exclude in the manifest. Can be provided multiple times.",
    multiple=True,
)
@click.option(
    "-ie",
    "--include-exclude-config",
    default=None,
    help="Include and exclude config of files and directories to include and exclude. Can be a json file or json string.",
    multiple=True,
)
@click.option(
    "--force-rehash",
    default=False,
    is_flag=True,
    help="Rehash all files to compare using file hashes.",
)
@click.option("--diff", default=None, help="File Path to Asset Manifest to diff against.")
@click.option("--json", default=None, is_flag=True, help="Output is printed as JSON for scripting.")
@_handle_error
def manifest_snapshot(
    root: str,
    destination: str,
    name: str,
    include: List[str],
    exclude: List[str],
    include_exclude_config: str,
    diff: str,
    force_rehash: bool,
    json: bool,
    **args,
):
    """
    BETA - Generates a snapshot of files in a directory root as a [job attachments] Manifest.

    [job attachments]: https://docs.aws.amazon.com/deadline-cloud/latest/userguide/storage-job-attachments.html
    """
    logger: ClickLogger = ClickLogger(is_json=json)
    if not os.path.isdir(root):
        raise NonValidInputError(f"Specified root directory {root} does not exist.")

    if destination and not os.path.isdir(destination):
        raise NonValidInputError(f"Specified destination directory {destination} does not exist.")
    elif destination is None:
        destination = root
        logger.echo(f"Manifest creation path defaulted to {root} \n")

    manifest_out = _manifest_snapshot(
        root=root,
        destination=destination,
        name=name,
        include=include,
        exclude=exclude,
        include_exclude_config=include_exclude_config,
        diff=diff,
        force_rehash=force_rehash,
        print_function_callback=logger.echo,
    )
    if manifest_out:
        if (
            sys.platform == "win32"
            and len(manifest_out.manifest) >= WINDOWS_MAX_PATH_LENGTH
            and not _is_windows_long_path_registry_enabled()
        ):
            long_manifest_path_warning = f"""WARNING: Manifest file path {manifest_out.manifest} exceeds Windows path length limit. This may cause unexpected issues.
For details and a fix using the registry, see: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation"""
            logger.echo(
                click.style(
                    long_manifest_path_warning,
                    fg="yellow",
                )
            )
            logger.json(
                dict(dataclasses.asdict(manifest_out), **{"warning": long_manifest_path_warning})
            )
        else:
            logger.json(dataclasses.asdict(manifest_out))


@cli_manifest.command(name="diff")
@click.option("--root", help="The root directory to compare changes to.")
@click.option(
    "--manifest",
    required=True,
    help="The path to manifest file to diff against.",
)
@click.option(
    "-i",
    "--include",
    default=None,
    help="Glob syntax of files and directories to include in the manifest. Can be provided multiple times.",
    multiple=True,
)
@click.option(
    "-e",
    "--exclude",
    default=None,
    help="Glob syntax of files and directories to exclude in the manifest. Can be provided multiple times.",
    multiple=True,
)
@click.option(
    "-ie",
    "--include-exclude-config",
    default=None,
    help="Include and exclude config of files and directories to include and exclude. Can be a json file or json string.",
)
@click.option(
    "--force-rehash",
    default=False,
    is_flag=True,
    help="Rehash all files to compare using file hashes.",
)
@click.option("--json", default=None, is_flag=True, help="Output is printed as JSON for scripting.")
@_handle_error
def manifest_diff(
    root: str,
    manifest: str,
    include: List[str],
    exclude: List[str],
    include_exclude_config: str,
    force_rehash: bool,
    json: bool,
    **args,
):
    """
    BETA - Compute the file difference of a root directory against an existing
    [job attachments] manifest for new, modified or deleted files.

    [job attachments]: https://docs.aws.amazon.com/deadline-cloud/latest/userguide/storage-job-attachments.html
    """
    logger: ClickLogger = ClickLogger(is_json=json)
    if not os.path.isfile(manifest):
        raise NonValidInputError(f"Specified manifest file {manifest} does not exist. ")

    if not os.path.isdir(root):
        raise NonValidInputError(f"Specified root directory {root} does not exist. ")

    # Perform the diff.
    differences: ManifestDiff = _manifest_diff(
        manifest=manifest,
        root=root,
        include=include,
        exclude=exclude,
        include_exclude_config=include_exclude_config,
        force_rehash=force_rehash,
        print_function_callback=logger.echo,
    )

    # Print results to console.
    if json:
        logger.json(dataclasses.asdict(differences), indent=4)
    else:
        logger.echo(f"Manifest Diff of root directory: {root}")
        all_files = _glob_files(
            root=root,
            include=include,
            exclude=exclude,
            include_exclude_config=include_exclude_config,
        )
        pretty_print_cli(root=root, all_files=all_files, manifest_diff=differences)


@cli_manifest.command(name="download")
@click.argument("download_dir")
@click.option("--profile", help="The AWS profile to use.")
@click.option("--job-id", required=True, help="The AWS Deadline Cloud Job to get. ")
@click.option("--step-id", help="The AWS Deadline Cloud Step to get. ")
@click.option("--farm-id", help="The AWS Deadline Cloud Farm to use. ")
@click.option("--queue-id", help="The AWS Deadline Cloud Queue to use. ")
@click.option(
    "--asset-type",
    default=AssetType.ALL.value,
    help="Which asset type to download:\n"
    "INPUT means download only input asset files for given job/step.\n"
    "OUTPUT means download only output asset files for given job/step.\n"
    "ALL (default) means download all input & output asset files for given job/step.\n",
    type=click.Choice(
        [e.value for e in AssetType],
        case_sensitive=False,
    ),
)
@click.option(
    "--json", default=None, is_flag=True, help="Output is printed as JSON for scripting. "
)
@_handle_error
def manifest_download(
    download_dir: str,
    job_id: str,
    step_id: str,
    asset_type: str,
    json: bool,
    **args,
):
    """
    BETA - Download [job attachments] Manifests for a Job, or Step including dependencies.

    [job attachments]: https://docs.aws.amazon.com/deadline-cloud/latest/userguide/storage-job-attachments.html
    """
    logger: ClickLogger = ClickLogger(is_json=json)
    if not os.path.isdir(download_dir):
        raise NonValidInputError(f"Specified destination directory {download_dir} does not exist. ")

    # setup config
    config: Optional[ConfigParser] = _apply_cli_options_to_config(
        required_options={"farm_id", "queue_id"}, **args
    )
    queue_id: str = config_file.get_setting("defaults.queue_id", config=config)
    farm_id: str = config_file.get_setting("defaults.farm_id", config=config)

    boto3_session: boto3.Session = api.get_boto3_session(config=config)

    output = _manifest_download(
        download_dir=download_dir,
        farm_id=farm_id,
        queue_id=queue_id,
        job_id=job_id,
        step_id=step_id,
        asset_type=AssetType(asset_type),
        boto3_session=boto3_session,
        print_function_callback=logger.echo,
    )
    logger.json(dataclasses.asdict(output))


@cli_manifest.command(name="upload")
@click.argument("manifest_file")
@click.option("--profile", help="The AWS profile to use.")
@click.option("--s3-cas-uri", help="The URI to the Content Addressable Storage S3 bucket and root.")
@click.option(
    "--s3-manifest-prefix", help="Prefix subpath in the manifest folder to upload the manifest."
)
@click.option(
    "--farm-id", help="The AWS Deadline Cloud Farm to use. Alternative to using --s3-cas-uri."
)
@click.option(
    "--queue-id", help="The AWS Deadline Cloud Queue to use. Alternative to using --s3-cas-uri."
)
@click.option("--json", default=None, is_flag=True, help="Output is printed as JSON for scripting.")
@_handle_error
def manifest_upload(
    manifest_file: str,
    s3_cas_uri: str,
    s3_manifest_prefix: str,
    json: bool,
    **args,
):
    """
    BETA - Uploads a [job attachments] manifest file to a Content Addressable Storage's Manifest store.
    If calling via --s3-cas-path, it is recommended to use with --profile for a specific AWS profile
    with CAS S3 bucket access. Check exit code for success or failure.

    [job attachments]: https://docs.aws.amazon.com/deadline-cloud/latest/userguide/storage-job-attachments.html
    """
    # Input checking.
    if not manifest_file or not os.path.isfile(manifest_file):
        raise NonValidInputError(f"Specified manifest {manifest_file} does not exist. ")

    # Where will we upload the manifest to?
    required: set[str] = set()
    if not s3_cas_uri:
        required = {"farm_id", "queue_id"}

    config: Optional[ConfigParser] = _apply_cli_options_to_config(required_options=required, **args)

    # Logger
    logger: ClickLogger = ClickLogger(is_json=json)

    bucket_name: str = ""
    session: boto3.Session = api.get_boto3_session(config=config)
    if not s3_cas_uri:
        farm_id = config_file.get_setting("defaults.farm_id", config=config)
        queue_id = config_file.get_setting("defaults.queue_id", config=config)

        deadline = api.get_boto3_client("deadline", config=config)
        queue = deadline.get_queue(
            farmId=farm_id,
            queueId=queue_id,
        )
        queue_ja_settings: JobAttachmentS3Settings = JobAttachmentS3Settings(
            **queue["jobAttachmentSettings"]
        )
        bucket_name = queue_ja_settings.s3BucketName
        cas_path = queue_ja_settings.rootPrefix

        # IF we supplied a farm and queue, use the queue credentials.
        session = api.get_queue_user_boto3_session(
            deadline=deadline,
            config=config,
            farm_id=farm_id,
            queue_id=queue_id,
            queue_display_name=queue["displayName"],
        )

    else:
        # Self supplied cas path.
        uri_ja_settings: JobAttachmentS3Settings = JobAttachmentS3Settings.from_s3_root_uri(
            s3_cas_uri
        )
        bucket_name = uri_ja_settings.s3BucketName
        cas_path = uri_ja_settings.rootPrefix

    logger.echo(
        f"Uploading Manifest to {bucket_name} {cas_path} {S3_MANIFEST_FOLDER_NAME}, prefix: {s3_manifest_prefix}"
    )
    _manifest_upload(
        manifest_file=manifest_file,
        s3_bucket_name=bucket_name,
        s3_cas_prefix=cas_path,
        s3_key_prefix=s3_manifest_prefix,
        boto_session=session,
        print_function_callback=logger.echo,
    )
    logger.echo("Uploading successful!")
