"""
A fake implementation of the Vuforia Web Services API.

See
https://library.vuforia.com/articles/Solution/How-To-Use-the-Vuforia-Web-Services-API
"""

import base64
import email.utils
import json
import os
import uuid
from http import HTTPStatus

import requests
from flask import Flask, Response, request

from mock_vws._constants import ResultCodes, TargetStatuses
from mock_vws._database_matchers import get_database_matching_server_keys
from mock_vws._mock_common import json_dump
from mock_vws._services_validators import run_services_validators
from mock_vws._services_validators.exceptions import (
    Fail,
    TargetStatusNotSuccess,
    TargetStatusProcessing,
    ValidatorException,
)
from mock_vws.database import VuforiaDatabase
from mock_vws.target import Target

VWS_FLASK_APP = Flask(import_name=__name__)
VWS_FLASK_APP.config["PROPAGATE_EXCEPTIONS"] = True


def get_all_databases() -> set[VuforiaDatabase]:
    """
    Get all database objects from the task manager back-end.
    """
    target_manager_base_url = os.environ["TARGET_MANAGER_BASE_URL"]
    response = requests.get(
        url=f"{target_manager_base_url}/databases",
        timeout=30,
    )
    return {
        VuforiaDatabase.from_dict(database_dict=database_dict)
        for database_dict in response.json()
    }


class ResponseNoContentTypeAdded(Response):
    """
    A custom response type.

    Without this, a content type is added to all responses.
    Some of our responses need to not have a "Content-Type" header.
    """

    default_mimetype = None


VWS_FLASK_APP.response_class = ResponseNoContentTypeAdded


@VWS_FLASK_APP.before_request
def set_terminate_wsgi_input() -> None:
    """
    We set ``wsgi.input_terminated`` to ``True`` when going through
    ``requests``, so that requests have the given ``Content-Length`` headers
    and the given data in ``request.headers`` and ``request.data``.

    We set this to ``False`` when running an application as standalone.
    This is because when running the Flask application, if this is set,
    reading ``request.data`` hangs.

    Therefore, when running the real Flask application, the behavior is not the
    same as the real Vuforia.
    This is documented as a difference in the documentation for this package.
    """
    terminate_wsgi_input = VWS_FLASK_APP.config.get(
        "TERMINATE_WSGI_INPUT",
        False,
    )
    request.environ["wsgi.input_terminated"] = terminate_wsgi_input


@VWS_FLASK_APP.before_request
def validate_request() -> None:
    """
    Run validators on the request.
    """
    databases = get_all_databases()
    run_services_validators(
        request_headers=dict(request.headers),
        request_body=request.data,
        request_method=request.method,
        request_path=request.path,
        databases=databases,
    )


@VWS_FLASK_APP.errorhandler(ValidatorException)
def handle_exceptions(exc: ValidatorException) -> Response:
    """
    Return the error response associated with the given exception.
    """
    return ResponseNoContentTypeAdded(
        status=exc.status_code.value,
        response=exc.response_text,
        headers=exc.headers,
    )


@VWS_FLASK_APP.route("/targets", methods=["POST"])
def add_target() -> Response:
    """
    Add a target.

    Fake implementation of
    https://library.vuforia.com/articles/Solution/How-To-Use-the-Vuforia-Web-Services-API.html#How-To-Add-a-Target
    """
    processing_time_seconds = float(
        os.environ.get("PROCESSING_TIME_SECONDS", "0.5"),
    )
    databases = get_all_databases()
    database = get_database_matching_server_keys(
        request_headers=dict(request.headers),
        request_body=request.data,
        request_method=request.method,
        request_path=request.path,
        databases=databases,
    )

    assert isinstance(database, VuforiaDatabase)

    # We do not use ``request.get_json(force=True)`` because this only works
    # when the content type is given as ``application/json``.
    request_json = json.loads(request.data)
    name = request_json["name"]
    active_flag = request_json.get("active_flag")
    if active_flag is None:
        active_flag = True

    new_target = Target(
        name=name,
        width=request_json["width"],
        image_value=base64.b64decode(request_json["image"]),
        active_flag=active_flag,
        processing_time_seconds=processing_time_seconds,
        application_metadata=request_json.get("application_metadata"),
    )

    target_manager_base_url = os.environ["TARGET_MANAGER_BASE_URL"]
    databases_url = f"{target_manager_base_url}/databases"
    requests.post(
        url=f"{databases_url}/{database.database_name}/targets",
        json=new_target.to_dict(),
        timeout=30,
    )

    date = email.utils.formatdate(None, localtime=False, usegmt=True)
    headers = {
        "content-type": "application/json",
        "server": "envoy",
        "date": date,
        "x-aws-region": "us-west-2, eu-west-1",
        "x-envoy-upstream-service-time": "5",
    }

    body = {
        "transaction_id": uuid.uuid4().hex,
        "result_code": ResultCodes.TARGET_CREATED.value,
        "target_id": new_target.target_id,
    }

    return Response(
        status=HTTPStatus.CREATED,
        response=json_dump(body),
        headers=headers,
    )


@VWS_FLASK_APP.route("/targets/<string:target_id>", methods=["GET"])
def get_target(target_id: str) -> Response:
    """
    Get details of a target.

    Fake implementation of
    https://library.vuforia.com/articles/Solution/How-To-Use-the-Vuforia-Web-Services-API.html#How-To-Retrieve-a-Target-Record
    """
    databases = get_all_databases()
    database = get_database_matching_server_keys(
        request_headers=dict(request.headers),
        request_body=request.data,
        request_method=request.method,
        request_path=request.path,
        databases=databases,
    )

    assert isinstance(database, VuforiaDatabase)
    [target] = [
        target for target in database.targets if target.target_id == target_id
    ]

    target_record = {
        "target_id": target.target_id,
        "active_flag": target.active_flag,
        "name": target.name,
        "width": target.width,
        "tracking_rating": target.tracking_rating,
        "reco_rating": target.reco_rating,
    }

    date = email.utils.formatdate(None, localtime=False, usegmt=True)
    headers = {
        "content-type": "application/json",
        "server": "envoy",
        "date": date,
        "x-aws-region": "us-west-2, eu-west-1",
        "x-envoy-upstream-service-time": "5",
    }
    body = {
        "result_code": ResultCodes.SUCCESS.value,
        "transaction_id": uuid.uuid4().hex,
        "target_record": target_record,
        "status": target.status,
    }
    return Response(
        status=HTTPStatus.OK,
        response=json_dump(body),
        headers=headers,
    )


@VWS_FLASK_APP.route("/targets/<string:target_id>", methods=["DELETE"])
def delete_target(target_id: str) -> Response:
    """
    Delete a target.

    Fake implementation of
    https://library.vuforia.com/articles/Solution/How-To-Use-the-Vuforia-Web-Services-API.html#How-To-Delete-a-Target
    """
    databases = get_all_databases()
    database = get_database_matching_server_keys(
        request_headers=dict(request.headers),
        request_body=request.data,
        request_method=request.method,
        request_path=request.path,
        databases=databases,
    )

    assert isinstance(database, VuforiaDatabase)
    [target] = [
        target for target in database.targets if target.target_id == target_id
    ]

    if target.status == TargetStatuses.PROCESSING.value:
        raise TargetStatusProcessing

    target_manager_base_url = os.environ["TARGET_MANAGER_BASE_URL"]
    databases_url = f"{target_manager_base_url}/databases"
    requests.delete(
        url=f"{databases_url}/{database.database_name}/targets/{target_id}",
        timeout=30,
    )

    body = {
        "transaction_id": uuid.uuid4().hex,
        "result_code": ResultCodes.SUCCESS.value,
    }
    date = email.utils.formatdate(None, localtime=False, usegmt=True)
    headers = {
        "content-type": "application/json",
        "server": "envoy",
        "date": date,
        "x-aws-region": "us-west-2, eu-west-1",
        "x-envoy-upstream-service-time": "5",
    }
    return Response(
        status=HTTPStatus.OK,
        response=json_dump(body),
        headers=headers,
    )


@VWS_FLASK_APP.route("/summary", methods=["GET"])
def database_summary() -> Response:
    """
    Get a database summary report.

    Fake implementation of
    https://library.vuforia.com/articles/Solution/How-To-Use-the-Vuforia-Web-Services-API.html#How-To-Get-a-Database-Summary-Report
    """
    databases = get_all_databases()
    database = get_database_matching_server_keys(
        request_headers=dict(request.headers),
        request_body=request.data,
        request_method=request.method,
        request_path=request.path,
        databases=databases,
    )

    assert isinstance(database, VuforiaDatabase)
    body = {
        "result_code": ResultCodes.SUCCESS.value,
        "transaction_id": uuid.uuid4().hex,
        "name": database.database_name,
        "active_images": len(database.active_targets),
        "inactive_images": len(database.inactive_targets),
        "failed_images": len(database.failed_targets),
        "target_quota": database.target_quota,
        "total_recos": database.total_recos,
        "current_month_recos": database.current_month_recos,
        "previous_month_recos": database.previous_month_recos,
        "processing_images": len(database.processing_targets),
        "reco_threshold": database.reco_threshold,
        "request_quota": database.request_quota,
        # We have ``self.request_count`` but Vuforia always shows 0.
        # This was not always the case.
        "request_usage": 0,
    }
    date = email.utils.formatdate(None, localtime=False, usegmt=True)
    headers = {
        "content-type": "application/json",
        "server": "envoy",
        "date": date,
        "x-aws-region": "us-west-2, eu-west-1",
        "x-envoy-upstream-service-time": "5",
    }
    return Response(
        status=HTTPStatus.OK,
        response=json_dump(body),
        headers=headers,
    )


@VWS_FLASK_APP.route("/summary/<string:target_id>", methods=["GET"])
def target_summary(target_id: str) -> Response:
    """
    Get a summary report for a target.

    Fake implementation of
    https://library.vuforia.com/articles/Solution/How-To-Use-the-Vuforia-Web-Services-API.html#How-To-Retrieve-a-Target-Summary-Report
    """
    databases = get_all_databases()
    database = get_database_matching_server_keys(
        request_headers=dict(request.headers),
        request_body=request.data,
        request_method=request.method,
        request_path=request.path,
        databases=databases,
    )

    assert isinstance(database, VuforiaDatabase)
    [target] = [
        target for target in database.targets if target.target_id == target_id
    ]
    body = {
        "status": target.status,
        "transaction_id": uuid.uuid4().hex,
        "result_code": ResultCodes.SUCCESS.value,
        "database_name": database.database_name,
        "target_name": target.name,
        "upload_date": target.upload_date.strftime("%Y-%m-%d"),
        "active_flag": target.active_flag,
        "tracking_rating": target.tracking_rating,
        "total_recos": target.total_recos,
        "current_month_recos": target.current_month_recos,
        "previous_month_recos": target.previous_month_recos,
    }
    date = email.utils.formatdate(None, localtime=False, usegmt=True)
    headers = {
        "content-type": "application/json",
        "server": "envoy",
        "date": date,
        "x-aws-region": "us-west-2, eu-west-1",
        "x-envoy-upstream-service-time": "5",
    }
    return Response(
        status=HTTPStatus.OK,
        response=json_dump(body),
        headers=headers,
    )


@VWS_FLASK_APP.route("/duplicates/<string:target_id>", methods=["GET"])
def get_duplicates(target_id: str) -> Response:
    """
    Get targets which may be considered duplicates of a given target.

    Fake implementation of
    https://library.vuforia.com/articles/Solution/How-To-Use-the-Vuforia-Web-Services-API.html#How-To-Check-for-Duplicate-Targets
    """
    databases = get_all_databases()
    database = get_database_matching_server_keys(
        request_headers=dict(request.headers),
        request_body=request.data,
        request_method=request.method,
        request_path=request.path,
        databases=databases,
    )

    assert isinstance(database, VuforiaDatabase)
    [target] = [
        target for target in database.targets if target.target_id == target_id
    ]
    other_targets = set(database.targets) - {target}

    similar_targets: list[str] = [
        other.target_id
        for other in other_targets
        if other.image_value == target.image_value
        and TargetStatuses.FAILED.value not in (target.status, other.status)
        and TargetStatuses.PROCESSING.value != other.status
        and other.active_flag
    ]

    body = {
        "transaction_id": uuid.uuid4().hex,
        "result_code": ResultCodes.SUCCESS.value,
        "similar_targets": similar_targets,
    }

    date = email.utils.formatdate(None, localtime=False, usegmt=True)
    headers = {
        "content-type": "application/json",
        "server": "envoy",
        "date": date,
        "x-aws-region": "us-west-2, eu-west-1",
        "x-envoy-upstream-service-time": "5",
    }
    return Response(
        status=HTTPStatus.OK,
        response=json_dump(body),
        headers=headers,
    )


@VWS_FLASK_APP.route("/targets", methods=["GET"])
def target_list() -> Response:
    """
    Get a list of all targets.

    Fake implementation of
    https://library.vuforia.com/articles/Solution/How-To-Use-the-Vuforia-Web-Services-API.html#How-To-Get-a-Target-List-for-a-Cloud-Database
    """
    databases = get_all_databases()
    database = get_database_matching_server_keys(
        request_headers=dict(request.headers),
        request_body=request.data,
        request_method=request.method,
        request_path=request.path,
        databases=databases,
    )
    assert isinstance(database, VuforiaDatabase)
    results = [target.target_id for target in database.not_deleted_targets]

    body = {
        "transaction_id": uuid.uuid4().hex,
        "result_code": ResultCodes.SUCCESS.value,
        "results": results,
    }
    date = email.utils.formatdate(None, localtime=False, usegmt=True)
    headers = {
        "content-type": "application/json",
        "server": "envoy",
        "date": date,
        "x-aws-region": "us-west-2, eu-west-1",
        "x-envoy-upstream-service-time": "5",
    }
    return Response(
        status=HTTPStatus.OK,
        response=json_dump(body),
        headers=headers,
    )


@VWS_FLASK_APP.route("/targets/<string:target_id>", methods=["PUT"])
def update_target(target_id: str) -> Response:
    """
    Update a target.

    Fake implementation of
    https://library.vuforia.com/articles/Solution/How-To-Use-the-Vuforia-Web-Services-API.html#How-To-Update-a-Target
    """
    # We do not use ``request.get_json(force=True)`` because this only works
    # when the content type is given as ``application/json``.
    request_json = json.loads(request.data)
    databases = get_all_databases()
    database = get_database_matching_server_keys(
        request_headers=dict(request.headers),
        request_body=request.data,
        request_method=request.method,
        request_path=request.path,
        databases=databases,
    )

    assert isinstance(database, VuforiaDatabase)
    [target] = [
        target for target in database.targets if target.target_id == target_id
    ]

    if target.status != TargetStatuses.SUCCESS.value:
        raise TargetStatusNotSuccess

    update_values = {}
    if "width" in request_json:
        update_values["width"] = request_json["width"]

    if "active_flag" in request_json:
        active_flag = request_json["active_flag"]
        if active_flag is None:
            raise Fail(status_code=HTTPStatus.BAD_REQUEST)
        update_values["active_flag"] = active_flag

    if "application_metadata" in request_json:
        application_metadata = request_json["application_metadata"]
        if application_metadata is None:
            raise Fail(status_code=HTTPStatus.BAD_REQUEST)
        update_values["application_metadata"] = application_metadata

    if "name" in request_json:
        name = request_json["name"]
        update_values["name"] = name

    if "image" in request_json:
        image = request_json["image"]
        update_values["image"] = image

    target_manager_base_url = os.environ["TARGET_MANAGER_BASE_URL"]
    put_url = (
        f"{target_manager_base_url}/databases/{database.database_name}/"
        f"targets/{target_id}"
    )
    requests.put(url=put_url, json=update_values, timeout=30)

    date = email.utils.formatdate(None, localtime=False, usegmt=True)
    headers = {
        "content-type": "application/json",
        "server": "envoy",
        "date": date,
        "x-aws-region": "us-west-2, eu-west-1",
        "x-envoy-upstream-service-time": "5",
    }
    body = {
        "result_code": ResultCodes.SUCCESS.value,
        "transaction_id": uuid.uuid4().hex,
    }
    return Response(
        status=HTTPStatus.OK,
        response=json_dump(body),
        headers=headers,
    )


if __name__ == "__main__":  # pragma: no cover
    VWS_FLASK_APP.run(debug=True, host=os.environ["VWS_HOST"])
