import os
import warnings
from pathlib import Path
from typing import List, Literal, Optional, Union

from sgqlc.operation import Operation

from ML_management.graphql import schema
from ML_management.graphql.schema import BuildArgInput, CustomImage, ImageFilterSettings, TimestampInterval
from ML_management.graphql.send_graphql_request import send_graphql_request
from ML_management.mlmanagement.log_api import _open_pipe_send_request, _raise_error
from ML_management.mlmanagement.utils import calculate_hash_directory, check_exclude_include, hash_file
from ML_management.mlmanagement.visibility_options import VisibilityOptions
from ML_management.sdk.job import _get_logs
from ML_management.variables import get_log_service_url


def _base_query_image(base):
    base.name()
    base.id()
    base.description()
    base.build_args()
    base.visibility()
    base.is_active()
    base.build_job.build_object_name()
    base.build_job.status()
    base.build_job.message()


def delete_image(image_id: int) -> bool:
    """
    Marks the image as inactive. You will no longer be able to run tasks with it, but you can view information.

    Parameters
    ----------
    image_id: int
         The name of the base image.
    Returns
    -------
    None
    """
    op = Operation(schema.Mutation)
    op.delete_images(image_ids=[image_id])
    return send_graphql_request(op=op)["deleteImages"]


def get_available_images() -> List[CustomImage]:
    """Get available images for custom code job.

    Returns
    -------
    List[CustomImage]
        List of instances of the CustomImage.

    """
    op = Operation(schema.Query)
    base = op.available_images()
    _base_query_image(base)

    result = send_graphql_request(op=op, json_response=False)
    return result.available_images


def _get_image_hash(hash_data: str) -> CustomImage:
    op = Operation(schema.Query)
    base = op.get_image_with_hash(hash_data=hash_data)
    _base_query_image(base)

    result = send_graphql_request(op=op, json_response=False)
    return result.get_image_with_hash


def rebuild_image(image_id: int, build_args: Optional[dict] = None) -> CustomImage:
    """
    Rebuild user image.

    Parameters
    ----------
    image_id: int
         The id of the base image.
    build_args: Optional[dict] = None
        A dictionary of build arguments.
        If None is passed, the previous parameters will be used.
    Returns
    -------
    CustomImage
        Instance of the CustomImage class.
    """
    if build_args is not None:
        build_args = [BuildArgInput(key=key, value=str(value)) for key, value in build_args.items()]
    op = Operation(schema.Mutation)
    base = op.rebuild_image(image_id=image_id, build_args=build_args)
    _base_query_image(base)

    result = send_graphql_request(op=op, json_response=False)
    return result.rebuild_image


def log_image(
    local_path: str,
    name: str,
    description: str,
    build_args: Optional[dict] = None,
    verbose: bool = True,
    exclude: list[str | Path] | None = None,
    include: list[str | Path] | None = None,
) -> CustomImage:
    """
    Log and build user image.

    Parameters
    ----------
    local_path: str
        Path to Dockerfile or to folder with Dockerfile.
    name: str
        The name of the image.
    build_args: Optional[dict] = None
        A dictionary of build arguments.
    description: str
        Description of the image.
    verbose: bool = True
        Whether to disable the entire progressbar wrapper.
    exclude: list[str | pathlib.Path] | None = None
        List the relative paths of the files within the logging folder that should be excluded from logging.
        For example: ["unused.py", "artifacts/modules/file.py"].
        Regular expressions are not currently supported.
        Parameters exclude and include are mutually exclusive and only one of them can be set.
    include: list[str | pathlib.Path] | None = None
        List the relative paths of only those files within the logging folder that should be included in logging.
        For example: ["model.py", "artifacts/modules/code.py"].
        Regular expressions are not currently supported.
        Parameters exclude and include are mutually exclusive and only one of them can be set.

    Returns
    -------
    CustomImage
        Instance of the CustomImage class.
    """
    local_path = os.path.abspath(local_path)
    if (os.path.isfile(local_path) and os.path.basename(local_path) != "Dockerfile") or (
        os.path.isdir(local_path) and "Dockerfile" not in os.listdir(local_path)
    ):
        raise Exception(
            "The passed local path must be the path to the Dockerfile or to the directory that contains it."
        )

    exclude, include = check_exclude_include(exclude, include)

    hash_data = calculate_hash_directory(local_path) if os.path.isdir(local_path) else hash_file(local_path)
    image = _get_image_hash(hash_data)
    if image:
        warnings.warn("Such an image already exists.")
        return image

    else:
        log_request = {"hash_data": hash_data, "name": name, "description": description}

        response = _open_pipe_send_request(
            local_path,
            log_request,
            url=get_log_service_url("log_image"),
            verbose=verbose,
            exclude=exclude,
            include=include,
        )
        _raise_error(response)
        result = response.json()
        image_id = result["image_id"]

        return rebuild_image(image_id, build_args)


def get_build_image_logs(
    name: str,
    stream: bool = True,
    file_name: Optional[str] = None,
) -> None:
    """
    Stream logs of the build job by image name.

    Parameters
    ----------
    name: str
        The name of the image.
    stream: bool = True
        Stream logs or dump all available at the moment.
    file_name: Optional[str] = None
        Name of the file where to save logs. Default: None. If None prints logs to the output.
    """
    _get_logs(
        object_type="image",
        stream=stream,
        file_name=file_name,
        params={"image_id": get_image_by_name(name).id},
    )


def get_build_image_logs_by_id(
    image_id: int,
    stream: bool = True,
    file_name: Optional[str] = None,
) -> None:
    """
    Stream logs of the build job by image id.

    Parameters
    ----------
    image_id: int
        The id of the image.
    stream: bool = True
        Stream logs or dump all available at the moment.
    file_name: Optional[str] = None
        Name of the file where to save logs. Default: None. If None prints logs to the output.
    """
    _get_logs(
        object_type="image",
        stream=stream,
        file_name=file_name,
        params={"image_id": image_id},
    )


def get_image(image_id: int) -> CustomImage:
    """
    Get image for custom code job.

    Parameters
    ----------
    image_id: int
    The id of the image.

    Returns
    -------
    CustomImage
        Instance of the CustomImage.

    """
    op = Operation(schema.Query)
    base = op.get_image(image_id=image_id)
    _base_query_image(base)

    result = send_graphql_request(op=op, json_response=False).get_image
    return result


def get_image_by_name(name: str) -> CustomImage:
    """
    Get image for custom code job.

    Parameters
    ----------
    name: str
    The name of the image.

    Returns
    -------
    CustomImage
        Instance of the CustomImage.

    """
    op = Operation(schema.Query)
    base = op.get_image_by_name(name=name)
    _base_query_image(base)

    result = send_graphql_request(op=op, json_response=False).get_image_by_name
    return result


def rename_image(image_id: int, new_name: str) -> CustomImage:
    """
    Rename image.

    Parameters
    ----------
    image_id: int
        The id of the image.
    new_name: str
        New image name.

    Returns
    -------
    CustomImage
        Instance of the CustomImage.
    """
    op = Operation(schema.Mutation)
    base = op.update_image(image_id=image_id, new_name=new_name)
    _base_query_image(base)

    image = send_graphql_request(op=op, json_response=False)
    return image.update_image


def set_image_description(image_id: int, description: str) -> CustomImage:
    """
    Set image description.

    Parameters
    ----------
    image_id: int
        The id of the image.
    description: str
        Description of an image.

    Returns
    -------
    CustomImage
        Instance of the CustomImage.
    """
    op = Operation(schema.Mutation)
    base = op.update_image(image_id=image_id, new_description=description)
    _base_query_image(base)

    image = send_graphql_request(op=op, json_response=False)
    return image.update_image


def pagination_image(
    name: Optional[str] = None,
    description: Optional[str] = None,
    visibility: Optional[Union[Literal["private", "public"], VisibilityOptions]] = None,
    owner_ids: Optional[list[str]] = None,
    build_end_from: Optional[int] = None,
    build_end_to: Optional[int] = None,
    limit: Optional[int] = None,
    offset: Optional[int] = None,
) -> List[CustomImage]:
    """
    Search images.

    Parameters
    ----------
    name: Optional[str]=None
        Name of the image.
    description: Optional[str]=None
        Description of the images.
    visibility: Optional[Union[Literal['private', 'public'], VisibilityOptions]]=None
        Visibility of image.
    owner_ids: Optional[list[str]]=None
        Ids of the image owner.
    build_end_from: Optional[int]=None
        Creation timestamp from of the model.
    build_end_to: Optional[int]=None
        Creation timestamp from of the model.
    limit: Optional[int] = None
        The maximum number of records that will be returned as a result.
    offset: Optional[int] = None
        The number of records that will be skipped before starting the selection.

    Returns
    -------
    List[CustomImage]
        List of CustomImage instance with meta information.
    """
    visibility = VisibilityOptions(visibility).name if visibility else visibility
    op = Operation(schema.Query)
    base_query = op.pagination_image(
        filter_settings=ImageFilterSettings(
            image_name=name,
            description=description,
            visibility=visibility,
            owner_ids=owner_ids,
            build_end_interval=TimestampInterval(start=build_end_from, end=build_end_to),
        ),
        limit=limit,
        offset=offset,
    ).list_image
    _base_query_image(base_query)

    return send_graphql_request(op, json_response=False).pagination_image.list_image


def cancel_build_job_for_image(image_id: int) -> bool:
    """
    Cancel running or planned build job of image.

    Parameters
    ----------
    image_id: int
        Id of the image.

    Returns
    -------
    bool
        Operation success status.
    """
    op = Operation(schema.Mutation)
    op.cancel_build_job_for_image(image_id=image_id)
    return send_graphql_request(op)["cancelBuildJobForImage"]
