import sys
from logging import Logger

from cfgtools import Group
from dialogmanager import DialogManager
from messagebus import MessageBus
from model import TestConnectionData
from modelcommon.customexception import SitefFinishPaymentRefundCanceled
from typing import (
    Optional,
    Dict,
    List,
)

import paymentprocessor
from util import Cache


class PaymentError(RuntimeError):
    def __repr__(self):
        return self.message


class PaymentNotMapped(PaymentError):
    def __init__(self, tender_id):
        self.message = "Payment {} not mapped".format(tender_id)

    def __repr__(self):
        return self.message


class ConfigurationNotFound(PaymentError):
    def __init__(self, configuration):
        self.message = "Configuration {} not found".format(configuration)


class PaymentNotFound(PaymentError):
    def __init__(self, payment_type):
        self.message = "Payment type {} not found".format(payment_type)


def _get_payment_class_reference(group_name):
    class_ref = getattr(sys.modules["paymentprocessor"], "{}PaymentProcessor".format(group_name), None)
    if class_ref is None:
        raise PaymentNotFound(group_name)
    return class_ref


class PaymentRouter(object):
    def __init__(
        self,
        message_bus,        # type: MessageBus
        dialog_manager,     # type: DialogManager
        config,             # type: Group
        logger,             # type: Logger
        cache,              # type: Cache
    ):
        self.message_bus = message_bus
        self.dialog_manager = dialog_manager
        self._config = config
        self._logger = logger
        self.cache = cache

        self._electronic_types = {}
        self._current_payment_processor = None  # type: Optional[paymentprocessor.PaymentProcessor]
        self._payment_route = {}                # type: Dict[int, paymentprocessor.PaymentProcessor]
        self._payment_processors = []           # type: List[paymentprocessor.PaymentProcessor]
        self._ask_via_pin_pad_routes = {}       # type: Dict[str, paymentprocessor.PaymentProcessor]
        self._fill_payment_routes()

    def _fill_payment_routes(self):
        try:
            payment_types = self._config.find_group("PaymentTypes")
            if not payment_types:
                raise ConfigurationNotFound("PaymentTypes")
            ask_document_processor = self._config.find_value("AskDocumentProcessor")

            for group in payment_types.groups:
                class_ref = _get_payment_class_reference(group_name=group.name)
                class_instance = class_ref(
                    message_bus=self.message_bus,
                    dialog_manager=self.dialog_manager,
                    config=group,
                    logger=self._logger,
                    cache=self.cache,
                )
                self._payment_processors.append(class_instance)
                for electronic_type in group.find_values("ElectronicTypes") or []:
                    self._electronic_types[int(electronic_type)] = True
                    self._payment_route[int(electronic_type)] = class_instance

                if ask_document_processor == group.name:
                    self._ask_via_pin_pad_routes["document"] = class_instance
        except Exception as _:
            self._logger.exception("Exception creating payment routes")
            raise

    def get_payment_processor(self, electronic_type=None):
        # type: (Optional[int]) -> paymentprocessor.PaymentProcessor

        if electronic_type is None:
            return self._current_payment_processor

        if electronic_type not in self._payment_route:
            raise PaymentNotMapped(electronic_type)

        self._current_payment_processor = self._payment_route[electronic_type]
        return self._current_payment_processor

    def get_ask_via_pin_pad_processor(
        self,
        route   # type: str
    ):
        # type: (...) -> paymentprocessor.PaymentProcessor

        if route not in self._ask_via_pin_pad_routes:
            raise PaymentNotMapped(route)

        return self._ask_via_pin_pad_routes[route]

    def get_connection_status(
        self,
        test_connection_data  # type: TestConnectionData
    ):
        # type: (...) -> bool

        validated_ips = []
        for payment_processor in self._payment_route.itervalues():
            ip_key = [x.values[0] for x in payment_processor.config.keys if x.name == "IP"]
            if len(ip_key) > 0:
                ip = ip_key[0]
                if ip in validated_ips:
                    continue

                validated_ips.append(ip)

            if payment_processor.connection_status(test_connection_data):
                return True

        return False

    def get_electronic_types(
        self
    ):
        # type: (...) -> Dict[int, bool]

        return self._electronic_types

    def get_name(
        self,
    ):
        # type: (...) -> str

        for payment_processor in self._payment_route.itervalues():
            return payment_processor.get_name()

    def get_last_receipt(
        self,
        eft_processor,     # type: str
    ):
        # type: (...) -> Optional[str]

        for payment_processor in self._payment_route.itervalues():
            return payment_processor.get_last_receipt(eft_processor=eft_processor)

    def stop_processors(self):
        for instance in self._payment_processors:
            instance.stop()

    def finish_all_payments(self, data):
        for processor in self._payment_processors:
            if not processor.finish_payment(data):
                raise SitefFinishPaymentRefundCanceled()

    def confirm_finish_payment(self, data):
        for processor in self._payment_processors:
            processor.confirm_finish_payment(data)
