import re
from collections import defaultdict

from parallels.core import messages
from parallels.core.migration_list.reader.base_reader import MigrationListReaderBase
from parallels.core.migration_list.entities.plans import MigrationListPlans
from parallels.core.migration_list.entities.list_data import MigrationListData
from parallels.core.migration_list.entities.ip_mapping import SubscriptionIPMapping
from parallels.core.migration_list.entities.subscription_info import SubscriptionMappingInfo
from parallels.core.utils.common import format_list
from parallels.core.utils.common.ip import is_ipv6, is_ipv4


class MigrationListReaderPlain(MigrationListReaderBase):
    _allowed_labels = ['plan', 'addon plan', 'customer', 'reseller', 'ipv4', 'ipv6']

    def read(self, fileobj, subscriptions_only=False, read_resellers=True):
        """Read migration list from file in plain text format

        Returns tuple, where:
        1) The first element is object which describes migration list data:
        parallels.core.migration_list.entities.list_data.MigrationListData
        2) The second element is list of error messages occurred when reading migration list.

        :param fileobj: file-like object to read migration list from
        :param bool subscriptions_only: whether to include subscriptions only, or also read plans, resellers, etc
        :param bool read_resellers: whether to read information about resellers; if false all subscriptions and
            customers owned by resellers will be owned by admin
        :rtype: tuple(parallels.core.migration_list.entities.list_data.MigrationListData, list[str | unicode))
        """
        source_subscriptions = self._source_data.get_source_subscriptions()
        handler = FullMigrationListHandler(
            self._allowed_labels, source_subscriptions, subscriptions_only, read_resellers
        )
        self._read(fileobj, handler)
        return handler.data, handler.errors

    def read_resellers(self, fileobj):
        """Read resellers from migration list file in plain text format

        Returns tuple, where:
        1) The first element is a dictionary, describing resellers:
        {reseller login: reseller plan}.
        If plan is not set and reseller subscription is custom, then reseller plan is None.
        2) The second element is list of error messages occurred when reading migration list.

        :param fileobj: file-like object to read migration list from
        :rtype: tuple(dict[str | unicode, str | unicode], list[str | unicode])
        """
        handler = ResellersMigrationListHandler(self._allowed_labels)
        self._read(fileobj, handler)

        return handler.resellers, handler.errors

    def read_plans(self, fileobj, read_resellers=True):
        """Read plans from migration list file in plain text format

        Returns tuple, where:
        1) The first element is object which describes migration list plans:
        parallels.core.migration_list.entities.plans.MigrationListPlans
        2) The second element is list of error messages occurred when reading migration list.

        :param fileobj: file-like object to read migration list from
        :param bool read_resellers: whether to read information about resellers;
            if false all plans under resellers are ignored
        :rtype: tuple(parallels.core.migration_list.entities.plans.MigrationListPlans, list[str | unicode])
        """
        handler = PlansMigrationListHandler(self._allowed_labels, read_resellers)
        self._read(fileobj, handler)

        return MigrationListPlans(handler.plans, handler.addon_plans), handler.errors

    @staticmethod
    def _read(fileobj, handler):
        """
        :type handler: BaseMigrationListLineHandler
        :rtype: None
        """
        for line_number, line in enumerate(fileobj, 1):
            line = line.strip('\r\n')

            line_matched = False
            if re.match("^\s*(#.*)?$", line, re.IGNORECASE) is not None:
                line_matched = True
            else:
                match = re.match("^\s*([^#]*?):\s?(\s*([^#]*)\s*(#.*)?)$", line, re.IGNORECASE)
                if match is not None:
                    label = match.group(1).strip().lower()
                    raw_value = match.group(2)
                    value = match.group(3).strip()
                    line_matched = handler.handle_label(label, value, raw_value, line_number)

                else:
                    match = re.match("^\s*([^#\s]*)\s*(#.*)?$", line, re.IGNORECASE)
                    if match is not None:
                        handler.handle_subscription(match.group(1).strip(), line_number)
                        line_matched = True

            if not line_matched:
                handler.handle_syntax_error(line, line_number)


class BaseMigrationListLineHandler(object):
    def __init__(self, allowed_labels):
        """
        :type allowed_labels: list[str | unicode]
        """
        self.errors = []
        self.allowed_labels = allowed_labels

    def handle_reseller(self, reseller, line_number):
        """Handle reseller label

        :type reseller: str | unicode
        :type line_number: int
        :rtype: None
        """
        pass

    def handle_reseller_plan(self, plan, line_number):
        """Handle reseller plan label

        :type plan: str | unicode
        :type line_number: int
        :rtype: None
        """
        pass

    def handle_plan(self, plan, line_number):
        """Handle hosting plan label

        :type plan: str | unicode
        :type line_number: int
        :rtype: None
        """
        pass

    def handle_addon_plan(self, addon_plan, line_number):
        """Handle hosting addon plan label

        :type addon_plan: str | unicode
        :type line_number: int
        :rtype: None
        """
        pass

    def handle_customer(self, customer, line_number):
        """Handle customer label

        :type customer: str | unicode
        :type line_number: int
        :rtype: None
        """
        pass

    def handle_ipv4(self, ipv4, line_number):
        """Handle IPv4 address label

        :type ipv4: str | unicode
        :type line_number: int
        :rtype: None
        """
        pass

    def handle_ipv6(self, ipv6, line_number):
        """Handle IPv6 address label

        :type ipv6: str | unicode
        :type line_number: int
        :rtype: None
        """
        pass

    def handle_node_ip(self, node_ip, line_number):
        """Handle IPv6 address label

        :type ipv6: str | unicode
        :type line_number: int
        :rtype: None
        """
        pass

    def handle_label(self, label, value, raw_value, line_number):
        """Handle label ("Key: value") in migration list file

        Return if some label matched (True) or not (False).

        Value is a raw value, but without any comments and leading spaces.
        So, for example for line "MyLabel:  my # value", raw value will be " my # value" while value will be "my".
        Raw value is used for data which could contain leading spaces and "#" symbol, for example names of plans.
        Value is used in all the other cases, for example for subscriptions and customers (which could not
        have spaces and "#" symbol according to Plesk valudataion rules).

        :type label: str | unicode
        :type value: str | unicode
        :type raw_value: str | unicode
        :type line_number: int
        :rtype: bool
        """
        label_handlers = {
            'customer': (self.handle_customer, False),
            'reseller': (self.handle_reseller, False),
            'plan': (self.handle_plan, True),
            'reseller plan': (self.handle_reseller_plan, True),
            'addon plan': (self.handle_addon_plan, True),
            'ipv4': (self.handle_ipv4, False),
            'ipv6': (self.handle_ipv6, False),
            'node ip': (self.handle_node_ip, False)
        }
        if label in label_handlers:
            handler, use_raw_value = label_handlers[label]
            handler(raw_value if use_raw_value else value, line_number)
            return True
        if label in self.allowed_labels:
            return True

        return False

    def handle_subscription(self, subscription, line_number):
        """Handle subscription line in migration list file

        :type subscription: str | unicode
        :type line_number: int
        :rtype: None
        """
        pass

    def handle_syntax_error(self, line, line_number):
        """Handle syntax error in migration list file - when line is not a subscription and no label matches

        :type line: str | unicode
        :type line_number: int
        :rtype: None
        """
        self.errors.append(messages.MIGRATION_LIST_INVALID_LINE.format(line=line_number, content=line))


class FullMigrationListHandler(BaseMigrationListLineHandler):
    def __init__(self, allowed_labels, source_subscriptions, subscriptions_only=False, read_resellers=True):
        super(FullMigrationListHandler, self).__init__(allowed_labels)
        self.data = MigrationListData(
            subscriptions_mapping={}, resellers={}, customers_mapping={}, plans={None: set()}
        )
        self.source_subscriptions = source_subscriptions
        self.subscriptions_only = subscriptions_only
        self._read_resellers = read_resellers
        self._reseller_plan = None
        self._inside_reseller = False
        self.current_reseller = None
        self.current_plan = None
        self.current_addon_plans = []
        self.current_customer = None
        self.current_ipv4 = None
        self.current_ipv6 = None
        self.current_node_ip = None

    def handle_reseller(self, reseller, line_number):
        if self.subscriptions_only:
            return
        if not self._read_resellers:
            self.current_plan = None
            self.current_addon_plans = []
            self.current_customer = None
            self._inside_reseller = True
            return

        self.current_reseller = reseller
        # The last reseller's plan always overrides the previous ones
        self.data.resellers[self.current_reseller] = self._reseller_plan
        if self.current_reseller not in self.data.plans:
            self.data.plans[self.current_reseller] = set()
        self.current_plan = None
        self.current_addon_plans = []
        self.current_customer = None
        self.current_ipv4 = None
        self.current_ipv6 = None
        self.current_node_ip = None

    def handle_customer(self, customer, line_number):
        if self.subscriptions_only:
            return

        self.current_customer = customer
        self.current_plan = None
        self.current_addon_plans = []

        if (
            self.current_customer in self.data.customers_mapping and
            self.data.customers_mapping[self.current_customer] != self.current_reseller
        ):
            self.errors.append(
                messages.MIGRATION_LIST_CUSTOMER_ASSIGNED_TO_ANOTHER_RESELLER.format(
                    line=line_number,
                    customer=self.current_customer,
                    reseller=self.data.customers_mapping[self.current_customer]
                )
            )
        self.data.customers_mapping[self.current_customer] = self.current_reseller

    def handle_plan(self, plan, line_number):
        if self.subscriptions_only:
            return
        if not self._read_resellers and self._inside_reseller:
            return

        self.current_plan = plan
        self.current_addon_plans = []
        self.data.plans[self.current_reseller].add(self.current_plan)

    def handle_addon_plan(self, addon_plan, line_number):
        if self.subscriptions_only:
            return
        if not self._read_resellers and self._inside_reseller:
            return

        self.current_addon_plans.append(addon_plan)
        # put addon plans in the same list as regular plans
        self.data.plans[self.current_reseller].add(addon_plan)

    def handle_reseller_plan(self, plan, line_number):
        if self.subscriptions_only or not self._read_resellers:
            return

        self._reseller_plan = plan

    def handle_subscription(self, subscription, line_number):
        if self._check_subscription(line_number, subscription):
            if self.current_customer is not None:
                owner = self.current_customer
            elif self.current_reseller is not None:
                owner = self.current_reseller
            else:
                owner = None  # special value for admin

            self.data.subscriptions_mapping[subscription] = SubscriptionMappingInfo(
                plan=self.current_plan,
                # create copy of the list so it is not modified for that subscription on the next unrelated addon plans
                addon_plans=list(self.current_addon_plans),
                owner=owner,
                ipv4=self.current_ipv4,
                ipv6=self.current_ipv6,
                node_ip=self.current_node_ip
            )

    def handle_ipv4(self, ipv4, line_number):
        if ipv4 in SubscriptionIPMapping.possible_ip_mapping_values() or is_ipv4(ipv4):
            self.current_ipv4 = ipv4
        else:
            self.errors.append(
                messages.MIGRATION_LIST_INVALID_IPV4.format(
                    line=line_number, address=ipv4,
                    possible_values=format_list(SubscriptionIPMapping.possible_ip_mapping_values())
                )
            )

    def handle_ipv6(self, ipv6, line_number):
        if ipv6 in SubscriptionIPMapping.possible_ip_mapping_values() or is_ipv6(ipv6):
            self.current_ipv6 = ipv6
        else:
            self.errors.append(
                messages.MIGRATION_LIST_INVALID_IPV6.format(
                    line=line_number, address=ipv6,
                    possible_values=format_list(SubscriptionIPMapping.possible_ip_mapping_values())
                )
            )

    def handle_node_ip(self, node_ip, line_number):
        self.current_node_ip = node_ip

    def _check_subscription(self, line_number, subscription):
        if subscription in self.data.subscriptions_mapping:
            self.errors.append(messages.MIGRATION_LIST_SUBSCRIPTION_IS_ALREADY_DEFINED.format(
                line=line_number, subscription=subscription
            ))
            return False
        elif subscription not in self.source_subscriptions:
            self.errors.append(messages.MIGRATION_LIST_SUBSCRIPTION_NOT_ON_SOURCE.format(
                line=line_number, subscription=subscription
            ))
            return False
        else:
            return True


class ResellersMigrationListHandler(BaseMigrationListLineHandler):
    def __init__(self, allowed_labels):
        super(ResellersMigrationListHandler, self).__init__(allowed_labels)
        self._resellers = dict()
        self._reseller_plan = None

    def handle_reseller(self, reseller, line_number):
        self._resellers[reseller] = self._reseller_plan

    def handle_reseller_plan(self, plan, line_number):
        self._reseller_plan = plan

    @property
    def resellers(self):
        """Get list of resellers read from migration list.
        Returns dictionary {reseller login: reseller plan}, if plan is not set and reseller
        subscription is custom, then reseller plan is None.

        :rtype: dict[str | unicode, str | unicode]
        """
        return self._resellers


class PlansMigrationListHandler(BaseMigrationListLineHandler):
    def __init__(self, allowed_labels, read_resellers=True):
        super(PlansMigrationListHandler, self).__init__(allowed_labels)
        self._read_resellers = read_resellers
        self.current_reseller = None
        self.plans = defaultdict(set)
        self.addon_plans = defaultdict(set)

    def handle_reseller(self, reseller, line_number):
        self.current_reseller = reseller

    def handle_plan(self, plan, line_number):
        if not self._read_resellers and self.current_reseller is not None:
            return

        self.plans[self.current_reseller].add(plan)

    def handle_addon_plan(self, addon_plan, line_number):
        if not self._read_resellers and self.current_reseller is not None:
            return

        self.addon_plans[self.current_reseller].add(addon_plan)
