from pathlib import Path

import click
from rich.console import Console

from bulk_restore_tool.__about__ import __version__
from bulk_restore_tool._client import Client
from bulk_restore_tool._db import V1BackupSetException
from bulk_restore_tool._job import RestoreJob
from bulk_restore_tool.models import DetailType
from bulk_restore_tool.models import JobDefinition
from bulk_restore_tool.models import FileSelection

console = Console(stderr=True)


def store_in_context(ctx, param, arg):
    """
    Callback for `click.option`s that stores option value in the `ctx.obj` (also ensures this is a dict).

    Useful for enabling "global" options that can be passed at any stage of the command or sub-commands.
    """
    if arg:
        ctx.ensure_object(dict)
        ctx.obj.update({param.name: arg})
    return arg


log_file_option = click.option(
    "--log-file",
    help="Specify location where logging gets written. Can also be set by BRT_LOG_FILE environment variable. "
    "Default='bulk_restore.log' in current directory.",
    callback=store_in_context,
    expose_value=False,
)
log_level_option = click.option(
    "--log-level",
    type=click.Choice(["CRITICAL", "WARN", "INFO", "DEBUG"]),
    help="Specify logging level for main logger. Can also be set by BRT_LOG_LEVEL environment variable. Defaults to INFO.",
    callback=store_in_context,
    expose_value=False,
)


def logging_options(f):
    f = log_file_option(f)
    f = log_level_option(f)
    return f


@click.group(
    context_settings={
        "help_option_names": ["-h", "--help"],
        "max_content_width": 200,
    },
    invoke_without_command=True,
    no_args_is_help=True,
)
@click.option(
    "--version", is_flag=True, help="Print the currently installed version of the tool."
)
@logging_options
def brt(version, **kwargs):
    """
    \b
    Code42 Bulk Restore Tool

    To authenticate, set the following variables either in your shell environment or an .env file in your
    current working directory:

    \b
        BRT_URL
        BRT_API_CLIENT_ID
        BRT_API_CLIENT_SECRET

    For details on API Client authentication, see: https://support.code42.com/Incydr/Admin/Code42_console_reference/API_clients
    """
    if version:
        print(__version__)


@brt.command()
@click.argument("id_", metavar="ID")
@click.option(
    "--name",
    help="Specify a name for the restore job. This name will be visible in audit log entries for the restore session. "
    "Default = A generated UUID.",
)
@click.option(
    "--type",
    "type_",
    type=click.Choice([d.value for d in DetailType]),
    default="archive",
    help="Specify what type of identifier to gather archive metadata for. Default = archive",
)
@click.option(
    "--target-folder",
    type=click.Path(),
    help="Specify target folder where archive metadata will be stored (will be created if it doesn't exist). "
    "Default = Directory `brt` is executed in.",
)
@click.option(
    "--definition-folder",
    type=click.Path(),
    help="Specify target folder the job definition .json file will be written to. "
    "Default = Directory `brt` is executed in.",
)
@click.option(
    "--zip",
    "zip_",
    is_flag=True,
    help="Restore files into .zip archives, one per device restored.",
)
@click.option(
    "--include-pattern",
    multiple=True,
    type=str,
    help="Specify regex pattern to select which file paths to include in the restore. Can be passed multiple times.",
)
@click.option(
    "--exclude-pattern",
    multiple=True,
    type=str,
    help="Specify regex pattern to select which file paths to exclude in the restore. Can be passed multiple times. A "
    "match on an exclude pattern will take precedence over any matching include patterns.",
)
@click.option(
    "--include-deleted",
    is_flag=True,
    help="Include files that have been marked as deleted in the archive file metadata. Default = false",
)
@click.option(
    "--max-deleted-date",
    type=click.DateTime(),
    help="Specify date for maximum age of deleted files to restore. Any files that were marked as deleted before this "
    "date will be excluded from restore. Default = None",
)
@click.option(
    "--as-of-date",
    type=click.DateTime(),
    help="Restore files as of specified date. Default = None",
)
@click.option(
    "--backup-set-ids",
    type=int,
    multiple=True,
    help="(V3 archives only) Specify which backup sets from the archive should be included, can be passed multiple "
    "times to include files from multiple sets. Default = Include files from all backup sets.",
)
@click.option(
    "--no-calculate",
    is_flag=True,
    help="Skip gathering of archive metadata and calculation of restore totals and only create job file. "
    "Default = false",
)
@logging_options
@click.pass_context
def create_job(
    ctx,
    id_,
    target_folder,
    definition_folder,
    type_,
    zip_,
    include_pattern,
    exclude_pattern,
    include_deleted,
    backup_set_ids,
    max_deleted_date,
    as_of_date,
    name,
    no_calculate,
):
    """
    Creates a restore job for a set of archives.

    Archives can be included in the job by specifying the identifier type (archive, device, username, or legalHold)
    via the --type option (defaults to archive) and providing a comma-separated list of identifiers as the main
    command-line argument. Some examples:

        \b
        brt create-job --type archive id1,id2,id3
        brt create-job --type username user@example.com
        brt create-job --type device 1234,4567
        brt create-job --type legalHold 7890

    If the --name option is not provided, a new UUID will be created as the job name and the parameters for the job will
    be saved in restore_<job_name>.json

    A <job_name> folder will also be created in the --target-folder path to store the archive metadata and restored
    files (if --target-folder option is not provided, it defaults to the current working directory).

    In the <job_name> folder, a human-readable .json file will be generated for each archive containing the file
    metadata from that archive. Additionally, a sqlite .db file will also be generated for each device in the job to
    track restore progress per device. Because a device can have multiple (destination) archives and/or multiple backup
    sets within a single archive, it's possible that some file paths will be duplicated within the full set of a single
    device's metadata. While duplicates will remain in the individual archive metadata files, they will be de-duplicated
    when restoring and when calculating the total file count and byte count of the restore job (the fileId with the most
    recent modification timestamp will be restored).
    """
    target_folder = Path(target_folder) if target_folder else Path(".")
    params = {
        "includeDeleted": include_deleted,
        "backupSetIds": backup_set_ids,
        "deletedMaxAge": max_deleted_date,
        "restoreAsOf": as_of_date,
        "includePatterns": include_pattern,
        "excludePatterns": exclude_pattern,
    }
    log_options = ctx.obj or {}
    client = Client(**log_options)
    ids = id_.split(",")
    archives = []
    for _id in ids:
        archives.extend(client.get_archive_details(_id, id_type=type_))
    job_definition = JobDefinition(
        type=type_,
        identifier=_id,
        selection=FileSelection(**params),
        archives=archives,
        targetFolder=target_folder.absolute(),
        zipResults=zip_,
    )
    if name:
        job_definition.jobId = name
    definition_folder = Path(definition_folder) if definition_folder else Path(".")
    definition_file = definition_folder / job_definition.filename
    with open(definition_file, "w") as f:
        f.write(job_definition.json(indent=2))
    if no_calculate:
        return
    job = RestoreJob(definition=job_definition)
    try:
        job.prepare()
        console.print(f"Job definition saved to: {definition_file}")
    except V1BackupSetException as err:
        console.print(f"[red]Error:[/red] {err.args[0]}")


@brt.command()
@click.option(
    "--quiet",
    "-q",
    is_flag=True,
    help="Silence output to stdout. Restore progress can be tracked via log files.",
)
@click.argument("JOB_FILE", type=click.File())
@logging_options
@click.option(
    "--device-log-level",
    type=click.Choice(["CRITICAL", "ERROR", "WARN", "INFO", "DEBUG"]),
    help="Specify level for loggers that write to the individual <deviceGuid>.log and <deviceGuid>_error.log files in "
    "the restore target directory. Can also be set by BRT_DEVICE_LOG_LEVEL environment variable. Defaults to INFO. "
    "Setting to CRITICAL effectively disables all device-specific logging.",
    callback=store_in_context,
    expose_value=False,
)
@click.pass_context
def start_job(ctx, job_file, quiet):
    """Begins restoring from archives defined in a given job file."""
    log_options = ctx.obj or {}
    job_definition = JobDefinition.parse_raw(job_file.read())
    job = RestoreJob(definition=job_definition, quiet=quiet, **log_options)
    try:
        job.start()
    except V1BackupSetException as err:
        console.print(f"[red]Error:[/red] {err.args[0]}")
