# Copyright (c) 2023 Airbyte, Inc., all rights reserved.

import functools
import json
from abc import ABC, abstractmethod
from pathlib import Path as FilePath
from typing import Any, Dict, List, Optional, Union

from airbyte_cdk.test.mock_http.response import HttpResponse
from airbyte_cdk.test.utils.data import get_unit_test_folder


def _extract(path: List[str], response_template: Dict[str, Any]) -> Any:
    return functools.reduce(lambda a, b: a[b], path, response_template)


def _replace_value(dictionary: Dict[str, Any], path: List[str], value: Any) -> None:
    current = dictionary
    for key in path[:-1]:
        current = current[key]
    current[path[-1]] = value


def _write(dictionary: Dict[str, Any], path: List[str], value: Any) -> None:
    current = dictionary
    for key in path[:-1]:
        current = current.setdefault(key, {})
    current[path[-1]] = value


class Path(ABC):
    @abstractmethod
    def write(self, template: Dict[str, Any], value: Any) -> None:
        pass

    @abstractmethod
    def update(self, template: Dict[str, Any], value: Any) -> None:
        pass

    def extract(self, template: Dict[str, Any]) -> Any:
        pass


class FieldPath(Path):
    def __init__(self, field: str):
        self._path = [field]

    def write(self, template: Dict[str, Any], value: Any) -> None:
        _write(template, self._path, value)

    def update(self, template: Dict[str, Any], value: Any) -> None:
        _replace_value(template, self._path, value)

    def extract(self, template: Dict[str, Any]) -> Any:
        return _extract(self._path, template)

    def __str__(self) -> str:
        return f"FieldPath(field={self._path[0]})"


class NestedPath(Path):
    def __init__(self, path: List[str]):
        self._path = path

    def write(self, template: Dict[str, Any], value: Any) -> None:
        _write(template, self._path, value)

    def update(self, template: Dict[str, Any], value: Any) -> None:
        _replace_value(template, self._path, value)

    def extract(self, template: Dict[str, Any]) -> Any:
        return _extract(self._path, template)

    def __str__(self) -> str:
        return f"NestedPath(path={self._path})"


class RootPath:
    """
    Path to use when the root of the response is an array.
    """

    def write(self, template: List[Dict[str, Any]], value: List[Dict[str, Any]]) -> None:
        template.extend(value)

    def update(self, template: List[Dict[str, Any]], value: List[Any]) -> None:
        template.clear()
        template.extend(value)

    def extract(self, template: List[Dict[str, Any]]) -> Any:
        return template

    def __str__(self) -> str:
        return f"RootPath"


class PaginationStrategy(ABC):
    @abstractmethod
    def update(self, response: Dict[str, Any]) -> None:
        pass


class FieldUpdatePaginationStrategy(PaginationStrategy):
    def __init__(self, path: Path, value: Any):
        self._path = path
        self._value = value

    def update(self, response: Dict[str, Any]) -> None:
        self._path.update(response, self._value)


class RecordBuilder:
    def __init__(
        self,
        template: Dict[str, Any],
        id_path: Optional[Path],
        cursor_path: Optional[Union[FieldPath, NestedPath]],
    ):
        self._record = template
        self._id_path = id_path
        self._cursor_path = cursor_path

        self._validate_template()

    def _validate_template(self) -> None:
        paths_to_validate = [
            ("_id_path", self._id_path),
            ("_cursor_path", self._cursor_path),
        ]
        for field_name, field_path in paths_to_validate:
            self._validate_field(field_name, field_path)

    def _validate_field(self, field_name: str, path: Optional[Path]) -> None:
        try:
            if path and not path.extract(self._record):
                raise ValueError(
                    f"{field_name} `{path}` was provided but it is not part of the template `{self._record}`"
                )
        except (IndexError, KeyError) as exception:
            raise ValueError(
                f"{field_name} `{path}` was provided but it is not part of the template `{self._record}`"
            ) from exception

    def with_id(self, identifier: Any) -> "RecordBuilder":
        self._set_field("id", self._id_path, identifier)
        return self

    def with_cursor(self, cursor_value: Any) -> "RecordBuilder":
        self._set_field("cursor", self._cursor_path, cursor_value)
        return self

    def with_field(self, path: Path, value: Any) -> "RecordBuilder":
        path.write(self._record, value)
        return self

    def _set_field(self, field_name: str, path: Optional[Path], value: Any) -> None:
        if not path:
            raise ValueError(
                f"{field_name}_path was not provided and hence, the record {field_name} can't be modified. Please provide `id_field` while "
                f"instantiating RecordBuilder to leverage this capability"
            )
        path.update(self._record, value)

    def build(self) -> Dict[str, Any]:
        return self._record


class HttpResponseBuilder:
    def __init__(
        self,
        template: Union[Dict[str, Any], List[Dict[str, Any]]],
        records_path: Union[FieldPath, NestedPath, RootPath],
        pagination_strategy: Optional[PaginationStrategy],
    ):
        _validate_path_with_response(records_path, template)

        self._records: List[RecordBuilder] = []
        self._response = template
        self._records_path = records_path
        self._pagination_strategy = pagination_strategy
        self._status_code = 200

    def with_record(self, record: RecordBuilder) -> "HttpResponseBuilder":
        self._records.append(record)
        return self

    def with_pagination(self) -> "HttpResponseBuilder":
        if not self._pagination_strategy:
            raise ValueError(
                "`pagination_strategy` was not provided and hence, fields related to the pagination can't be modified. Please provide "
                "`pagination_strategy` while instantiating ResponseBuilder to leverage this capability"
            )
        elif isinstance(self._response, List):
            raise ValueError("pagination_strategy requires the response to be a dict but was list")

        self._pagination_strategy.update(self._response)
        return self

    def with_status_code(self, status_code: int) -> "HttpResponseBuilder":
        self._status_code = status_code
        return self

    def build(self) -> HttpResponse:
        self._records_path.update(self._response, [record.build() for record in self._records])  # type: ignore  # validated using _validate_path_with_response
        return HttpResponse(json.dumps(self._response), self._status_code)


def _get_unit_test_folder(execution_folder: str) -> FilePath:
    # FIXME: This function should be removed after the next CDK release to avoid breaking amazon-seller-partner test code.
    return get_unit_test_folder(execution_folder)


def find_template(resource: str, execution_folder: str) -> Dict[str, Any]:
    response_template_filepath = str(
        get_unit_test_folder(execution_folder)
        / "resource"
        / "http"
        / "response"
        / f"{resource}.json"
    )
    with open(response_template_filepath, "r") as template_file:
        return json.load(template_file)  # type: ignore  # we assume the dev correctly set up the resource file


def find_binary_response(resource: str, execution_folder: str) -> bytes:
    response_filepath = str(
        get_unit_test_folder(execution_folder) / "resource" / "http" / "response" / f"{resource}"
    )
    with open(response_filepath, "rb") as response_file:
        return response_file.read()  # type: ignore  # we assume the dev correctly set up the resource file


def create_record_builder(
    response_template: Dict[str, Any],
    records_path: Union[FieldPath, NestedPath, RootPath],
    record_id_path: Optional[Path] = None,
    record_cursor_path: Optional[Union[FieldPath, NestedPath]] = None,
) -> RecordBuilder:
    """
    This will use the first record define at `records_path` as a template for the records. If more records are defined, they will be ignored
    """
    _validate_path_with_response(records_path, response_template)
    try:
        record_template = records_path.extract(response_template)[0]  # type: ignore  # validated using _validate_path_with_response
        if not record_template:
            raise ValueError(
                f"Could not extract any record from template at path `{records_path}`. "
                f"Please fix the template to provide a record sample or fix `records_path`."
            )
        return RecordBuilder(record_template, record_id_path, record_cursor_path)
    except (IndexError, KeyError):
        raise ValueError(
            f"Error while extracting records at path `{records_path}` from response template `{response_template}`"
        )


def create_response_builder(
    response_template: Union[Dict[str, Any], List[Dict[str, Any]]],
    records_path: Union[FieldPath, NestedPath, RootPath],
    pagination_strategy: Optional[PaginationStrategy] = None,
) -> HttpResponseBuilder:
    return HttpResponseBuilder(response_template, records_path, pagination_strategy)


def _validate_path_with_response(
    records_path: Union[FieldPath, NestedPath, RootPath],
    response_template: Union[Dict[str, Any], List[Dict[str, Any]]],
) -> None:
    if isinstance(response_template, List) and not isinstance(records_path, RootPath):
        raise ValueError("templates of type lists require RootPath")
    elif isinstance(response_template, Dict) and not isinstance(
        records_path, (FieldPath, NestedPath)
    ):
        raise ValueError("templates of type dict either require FieldPath or NestedPath")
