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


from abc import ABC
from typing import Any, Dict, Iterable, List, Mapping, MutableMapping, Optional, Tuple

import pendulum
import requests
from airbyte_cdk.models import SyncMode
from airbyte_cdk.sources import AbstractSource
from airbyte_cdk.sources.streams import Stream
from airbyte_cdk.sources.streams.http import HttpStream
from airbyte_cdk.sources.streams.http.auth import HttpAuthenticator
from airbyte_cdk.sources.utils.transform import TransformConfig, TypeTransformer


class BigcommerceStream(HttpStream, ABC):
    # Latest Stable Release
    api_version = "v3"
    # Page size
    limit = 250
    # Define primary key as sort key for full_refresh, or very first sync for incremental_refresh
    primary_key = "id"
    order_field = "date_modified:asc"
    filter_field = "date_modified:min"
    data = "data"

    transformer: TypeTransformer = TypeTransformer(TransformConfig.DefaultSchemaNormalization | TransformConfig.CustomSchemaNormalization)

    def __init__(self, start_date: str, store_hash: str, access_token: str, **kwargs):
        super().__init__(**kwargs)
        self.start_date = start_date
        self.store_hash = store_hash
        self.access_token = access_token

    @transformer.registerCustomTransform
    def transform_function(original_value: Any, field_schema: Dict[str, Any]) -> Any:
        """
        This functions tries to handle the various date-time formats BigCommerce API returns and normalize the values to isoformat.
        """
        if "format" in field_schema and field_schema["format"] == "date-time":
            if not original_value:  # Some dates are empty strings: "".
                return None
            transformed_value = None
            supported_formats = ["YYYY-MM-DD", "YYYY-MM-DDTHH:mm:ssZZ", "YYYY-MM-DDTHH:mm:ss[Z]", "ddd, D MMM YYYY HH:mm:ss ZZ"]
            for format in supported_formats:
                try:
                    transformed_value = str(pendulum.from_format(original_value, format))  # str() returns isoformat
                except ValueError:
                    continue
            if not transformed_value:
                raise ValueError(f"Unsupported date-time format for {original_value}")
            return transformed_value
        return original_value

    @property
    def url_base(self) -> str:
        return f"https://api.bigcommerce.com/stores/{self.store_hash}/{self.api_version}/"

    def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, Any]]:
        json_response = response.json()
        meta = json_response.get("meta", None)
        if meta:
            pagination = meta.get("pagination", None)
            if pagination and pagination.get("current_page") < pagination.get("total_pages"):
                return dict(page=pagination.get("current_page") + 1)
            else:
                return None

    def request_params(
        self, stream_state: Mapping[str, Any], stream_slice: Mapping[str, any] = None, next_page_token: Mapping[str, Any] = None
    ) -> MutableMapping[str, Any]:
        params = {"limit": self.limit}
        params.update({"sort": self.order_field})
        if next_page_token:
            params.update(**next_page_token)
        else:
            params[self.filter_field] = self.start_date
        return params

    def request_headers(
        self, stream_state: Mapping[str, Any], stream_slice: Mapping[str, Any] = None, next_page_token: Mapping[str, Any] = None
    ) -> Mapping[str, Any]:
        headers = super().request_headers(stream_state=stream_state, stream_slice=stream_slice, next_page_token=next_page_token)
        headers.update({"Accept": "application/json", "Content-Type": "application/json"})
        return headers

    def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]:
        json_response = response.json()
        records = json_response.get(self.data, []) if self.data is not None else json_response
        yield from records


class IncrementalBigcommerceStream(BigcommerceStream, ABC):
    # Getting page size as 'limit' from parent class
    @property
    def limit(self):
        return super().limit

    # Setting the check point interval to the limit of the records output
    state_checkpoint_interval = limit
    # Setting the default cursor field for all streams
    cursor_field = "date_modified"

    def get_updated_state(self, current_stream_state: MutableMapping[str, Any], latest_record: Mapping[str, Any]) -> Mapping[str, Any]:
        return {self.cursor_field: max(latest_record.get(self.cursor_field, ""), current_stream_state.get(self.cursor_field, ""))}

    def request_params(self, stream_state: Mapping[str, Any] = None, next_page_token: Mapping[str, Any] = None, **kwargs):
        params = super().request_params(stream_state=stream_state, next_page_token=next_page_token, **kwargs)
        # If there is a next page token then we should only send pagination-related parameters.
        if stream_state:
            params[self.filter_field] = stream_state.get(self.cursor_field)
        else:
            params[self.filter_field] = self.start_date
        return params

    def filter_records_newer_than_state(self, stream_state: Mapping[str, Any] = None, records_slice: Mapping[str, Any] = None) -> Iterable:
        if stream_state:
            for record in records_slice:
                if record[self.cursor_field] >= stream_state.get(self.cursor_field):
                    yield record
        else:
            yield from records_slice


class OrderSubstream(IncrementalBigcommerceStream):
    def read_records(
        self, stream_state: Mapping[str, Any] = None, stream_slice: Optional[Mapping[str, Any]] = None, **kwargs
    ) -> Iterable[Mapping[str, Any]]:
        orders_stream = Orders(
            authenticator=self.authenticator, start_date=self.start_date, store_hash=self.store_hash, access_token=self.access_token
        )
        for data in orders_stream.read_records(sync_mode=SyncMode.full_refresh):
            slice = super().read_records(stream_slice={"order_id": data["id"]}, **kwargs)
            yield from self.filter_records_newer_than_state(stream_state=stream_state, records_slice=slice)


class Customers(IncrementalBigcommerceStream):
    data_field = "customers"

    def path(self, **kwargs) -> str:
        return f"{self.data_field}"


class Products(IncrementalBigcommerceStream):
    data_field = "products"
    # Override `order_field` because Products API does not accept `asc` value
    order_field = "date_modified"

    def path(self, **kwargs) -> str:
        return f"catalog/{self.data_field}"


class Orders(IncrementalBigcommerceStream):
    data_field = "orders"
    api_version = "v2"
    order_field = "date_modified:asc"
    filter_field = "min_date_modified"
    page = 1

    def path(self, **kwargs) -> str:
        return f"{self.data_field}"

    def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]:
        return response.json() if len(response.content) > 0 else []

    def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, Any]]:
        if len(response.content) > 0 and len(response.json()) == self.limit:
            self.page = self.page + 1
            return dict(page=self.page)
        else:
            return None


class Pages(IncrementalBigcommerceStream):
    data_field = "pages"
    cursor_field = "id"

    def path(self, **kwargs) -> str:
        return f"content/{self.data_field}"

    def get_updated_state(self, current_stream_state: MutableMapping[str, Any], latest_record: Mapping[str, Any]) -> Mapping[str, Any]:
        return {self.cursor_field: max(latest_record.get(self.cursor_field, 0), current_stream_state.get(self.cursor_field, 0))}

    def read_records(
        self, stream_state: Mapping[str, Any] = None, stream_slice: Optional[Mapping[str, Any]] = None, **kwargs
    ) -> Iterable[Mapping[str, Any]]:
        slice = super().read_records(sync_mode=SyncMode.full_refresh, stream_slice=stream_slice, stream_state=stream_state)
        yield from self.filter_records_newer_than_state(stream_state=stream_state, records_slice=slice)


class Brands(IncrementalBigcommerceStream):
    data_field = "brands"
    cursor_field = "id"
    order_field = "id"

    def path(self, **kwargs) -> str:
        return f"catalog/{self.data_field}"

    def request_params(
        self, stream_state: Mapping[str, Any], stream_slice: Mapping[str, any] = None, next_page_token: Mapping[str, Any] = None
    ) -> MutableMapping[str, Any]:
        params = {"limit": self.limit}
        params.update({"sort": self.order_field})
        if next_page_token:
            params.update(**next_page_token)
        return params

    def get_updated_state(self, current_stream_state: MutableMapping[str, Any], latest_record: Mapping[str, Any]) -> Mapping[str, Any]:
        return {self.cursor_field: max(latest_record.get(self.cursor_field, 0), current_stream_state.get(self.cursor_field, 0))}

    def read_records(
        self, stream_state: Mapping[str, Any] = None, stream_slice: Optional[Mapping[str, Any]] = None, **kwargs
    ) -> Iterable[Mapping[str, Any]]:
        slice = super().read_records(sync_mode=SyncMode.full_refresh, stream_slice=stream_slice, stream_state=stream_state)
        yield from self.filter_records_newer_than_state(stream_state=stream_state, records_slice=slice)


class Categories(IncrementalBigcommerceStream):
    data_field = "categories"
    cursor_field = "id"
    order_field = "id"

    def path(self, **kwargs) -> str:
        return f"catalog/{self.data_field}"

    def request_params(
        self, stream_state: Mapping[str, Any], stream_slice: Mapping[str, any] = None, next_page_token: Mapping[str, Any] = None
    ) -> MutableMapping[str, Any]:
        params = {"limit": self.limit}
        params.update({"sort": self.order_field})
        if next_page_token:
            params.update(**next_page_token)
        return params

    def get_updated_state(self, current_stream_state: MutableMapping[str, Any], latest_record: Mapping[str, Any]) -> Mapping[str, Any]:
        return {self.cursor_field: max(latest_record.get(self.cursor_field, 0), current_stream_state.get(self.cursor_field, 0))}

    def read_records(
        self, stream_state: Mapping[str, Any] = None, stream_slice: Optional[Mapping[str, Any]] = None, **kwargs
    ) -> Iterable[Mapping[str, Any]]:
        slice = super().read_records(sync_mode=SyncMode.full_refresh, stream_slice=stream_slice, stream_state=stream_state)
        yield from self.filter_records_newer_than_state(stream_state=stream_state, records_slice=slice)


class Transactions(OrderSubstream):
    data_field = "transactions"
    cursor_field = "id"

    def path(self, stream_slice: Mapping[str, Any] = None, **kwargs) -> str:
        order_id = stream_slice["order_id"]
        return f"orders/{order_id}/{self.data_field}"

    def get_updated_state(self, current_stream_state: MutableMapping[str, Any], latest_record: Mapping[str, Any]) -> Mapping[str, Any]:
        return {self.cursor_field: max(latest_record.get(self.cursor_field, 0), current_stream_state.get(self.cursor_field, 0))}

    def request_params(
        self, stream_state: Mapping[str, Any] = None, next_page_token: Mapping[str, Any] = None, **kwargs
    ) -> MutableMapping[str, Any]:
        params = {"limit": self.limit}
        return params


class OrderProducts(OrderSubstream):
    api_version = "v2"
    data_field = "products"
    cursor_field = "id"

    def path(self, stream_slice: Mapping[str, Any] = None, **kwargs) -> str:
        order_id = stream_slice["order_id"]
        return f"orders/{order_id}/{self.data_field}"

    def get_updated_state(self, current_stream_state: MutableMapping[str, Any], latest_record: Mapping[str, Any]) -> Mapping[str, Any]:
        return {self.cursor_field: max(latest_record.get(self.cursor_field, 0), current_stream_state.get(self.cursor_field, 0))}

    def request_params(
        self, stream_state: Mapping[str, Any] = None, next_page_token: Mapping[str, Any] = None, **kwargs
    ) -> MutableMapping[str, Any]:
        params = {"limit": self.limit}
        return params

    def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]:
        return response.json() if len(response.content) > 0 else []

    def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, Any]]:
        if len(response.content) > 0 and len(response.json()) == self.limit:
            self.page = self.page + 1
            return dict(page=self.page)
        else:
            return None


class Channels(IncrementalBigcommerceStream):
    data_field = "channels"
    # Override `order_field` bacause Channels API do not acept `asc` value
    order_field = "date_modified"

    def path(self, **kwargs) -> str:
        return f"{self.data_field}"


class Store(BigcommerceStream):
    data_field = "store"
    cursor_field = "store_id"
    api_version = "v2"
    data = None

    def path(self, **kwargs) -> str:
        return f"{self.data_field}"

    def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]:
        json_response = response.json()
        yield from [json_response]


class BigcommerceAuthenticator(HttpAuthenticator):
    def __init__(self, token: str):
        self.token = token

    def get_auth_header(self) -> Mapping[str, Any]:
        return {"X-Auth-Token": f"{self.token}"}


class SourceBigcommerce(AbstractSource):
    def check_connection(self, logger, config) -> Tuple[bool, any]:
        store_hash = config["store_hash"]
        access_token = config["access_token"]
        api_version = "v3"

        headers = {"X-Auth-Token": access_token, "Accept": "application/json", "Content-Type": "application/json"}
        url = f"https://api.bigcommerce.com/stores/{store_hash}/{api_version}/channels"

        try:
            session = requests.get(url, headers=headers)
            session.raise_for_status()
            return True, None
        except requests.exceptions.RequestException as e:
            return False, e

    def streams(self, config: Mapping[str, Any]) -> List[Stream]:

        auth = BigcommerceAuthenticator(token=config["access_token"])
        args = {
            "authenticator": auth,
            "start_date": config["start_date"],
            "store_hash": config["store_hash"],
            "access_token": config["access_token"],
        }
        return [
            Customers(**args),
            Pages(**args),
            Orders(**args),
            Transactions(**args),
            Products(**args),
            Channels(**args),
            Store(**args),
            OrderProducts(**args),
            Brands(**args),
            Categories(**args),
        ]
