import logging
from pathlib import Path

import requests
from requests import RequestException
from tenacity import (
    retry,
    retry_if_exception_type,
    stop_after_attempt,
    wait_chain,
    wait_fixed,
)

from arkindex import ArkindexClient, should_verify_cert
from arkindex.client.client import REQUEST_TIMEOUT
from arkindex.exceptions import ArkindexConflictError, ErrorResponse

logger = logging.getLogger(__name__)


@retry(
    reraise=True,
    retry=retry_if_exception_type(RequestException),
    stop=stop_after_attempt(3),
    wait=wait_fixed(5),
)
def upload_file(path: str | Path, url: str) -> None:
    with open(path, "rb") as f:
        # Directly use requests to upload the image, as the api_client.session.auth would conflict with the s3 API
        resp = requests.put(url, data=f, verify=should_verify_cert(url), timeout=REQUEST_TIMEOUT)
    resp.raise_for_status()


@retry(
    # Retry on any error, including HTTP 4xx because failed checks with handled errors can cause HTTP 400
    retry=retry_if_exception_type((RequestException, ErrorResponse)),
    wait=wait_chain(wait_fixed(10), wait_fixed(30)),
    stop=stop_after_attempt(3),
    reraise=True,
)
def check_image(api_client: ArkindexClient, image: dict, *, force: bool = False) -> dict:
    """
    Perform an image check on an existing image using `PartialUpdateImage`.

    :param api_client: An API client instance to use to make API requests.
    :type api_client: ArkindexClient
    :param image: A dict describing the image stored on Arkindex, as returned by `CreateImage` or `RetrieveImage`.
    :type image: dict
    :param force: Always run the image check, even if the image is already `checked`.
    :type force: bool
    :return: The updated image.
    :rtype: dict
    """
    if image["status"] == "checked" and not force:
        logger.debug(f"Skipping image check for image {image['id']} as it is already checked")
        return image

    logger.info(f"Checking image {image['id']}")
    image = api_client.request("PartialUpdateImage", id=image["id"], body={"status": "checked"})
    assert image["status"] == "checked", "Image was not marked as checked"
    return image


def create_image(
    api_client: ArkindexClient,
    path: str | Path,
    arkindex_path: str,
    *,
    force_check: bool = False,
) -> dict:
    """
    Upload an image from a local file into Arkindex. This handles already existing images and performs image checks.

    :param api_client: An API client instance to use to make API requests.
    :type api_client: ArkindexClient
    :param path: Path to a local file to use as the new image.
    :type path: Path
    :param arkindex_path: Destination path of the image within the local ImageServer.
        This is the `path` parameter of `CreateImage`.
    :type arkindex_path: str
    :param force_check: Call `PartialUpdateImage` to perform a new image check,
        even when the image is already marked as `checked`.
    :type force_check: bool
    :return: A dict describing the image stored on Arkindex, as returned by `CreateImage` or `RetrieveImage`.
    :rtype: dict
    """
    try:
        image = api_client.request("CreateImage", body={"path": arkindex_path})
    except ArkindexConflictError as e:
        # Handle errors on Arkindex ≥ 1.11.2
        if not e.content["existing_id"]:
            raise
        image = api_client.request("RetrieveImage", id=e.content["existing_id"])
    except ErrorResponse as e:
        # Handle errors on older versions
        if e.status_code != 400 or not isinstance(e.content, dict) or not e.content.get("id"):
            raise
        image = api_client.request("RetrieveImage", id=e.content["id"])

    if image["status"] != "checked":
        logger.info(f"Uploading image {image['id']}")
        assert image["s3_put_url"], "No S3 PUT URL was returned for this image."
        upload_file(path, image["s3_put_url"])

    return check_image(api_client, image, force=force_check)


def create_iiif_image(
    api_client: ArkindexClient,
    url: str,
    *,
    width: int | None = None,
    height: int | None = None,
) -> dict:
    """
    Import an IIIF image into Arkindex. This handles already existing images and performs image checks.

    :param api_client: An API client instance to use to make API requests.
    :type api_client: ArkindexClient
    :param url: The URL of the image's identifier.
       This should not contain any parameters like `/full/full/0/default.jpg`,
       and should not be the URL to the Image Information request (`/info.json`).
    :type url: str
    :param width: Width of the image in pixels.

       This is only allowed when the image is on a known ImageServer where external validation has been allowed,
       and when the user has the permission to trigger external validation. `height` must also be set.
    :type width: int | None
    :param height: Height of the image in pixels.

       This is only allowed when the image is on a known ImageServer where external validation has been allowed,
       and when the user has the permission to trigger external validation. `width` must also be set.
    :type height: int | None
    :return: A dict describing the image stored on Arkindex, as returned by `CreateIIIFURL` or `RetrieveImage`.
    :rtype: dict
    """
    assert not (width is None) ^ (height is None), "The width and height must either be both set or both unset."

    try:
        image = api_client.request(
            "CreateIIIFURL",
            body={"url": url, "width": width, "height": height},
        )
    except ArkindexConflictError as e:
        # Handle errors on Arkindex ≥ 1.11.2
        if not e.content["existing_id"]:
            raise
        image = api_client.request("RetrieveImage", id=e.content["existing_id"])
    except ErrorResponse as e:
        # Handle errors on older versions
        if e.status_code != 400 or not isinstance(e.content, dict) or not e.content.get("id"):
            raise
        image = api_client.request("RetrieveImage", id=e.content["id"])

    # Do not perform any check if the width and height were sent, because they are triggering an async check
    if width or height:
        return image
    return check_image(api_client, image)
