# -*- coding: utf-8 -*-

from collections import defaultdict

from ppms import ppms
from ppms.migration import ChangeDTPPacket, SQLPacket, PacketCategory,\
    MinVersionDependency, MaxVersionDependency, VersionType


def partition(seq, key):
    d = defaultdict(list)
    for x in seq:
        d[key(x)].append(x)
    return d


def tuple_get_first(t):
    return t[0]


class DataField():
    """Data Field helper class.

    Trivial Data Field wrapper. Merely contains its own ID (DataArea+DataField composite key) and the DI it shows, plus
    a couple of wrapper methods.
    """

    data_area = None
    data_field = None
    data_item = None
    aggregation_type = None

    def __init__(self, df):
        self.data_area = df[0]
        self.data_field = int(df[1])
        self.data_item = ppms.get_data_dictionary().data_items[int(df[2])]
        self.aggregation_type = int(df[3])

    def __equals__(self, other):
        return self.data_area == other.data_area and self.data_field == other.data_field

    def __repr__(self):
        return '%s (DA %s, DF %s, DI %s, aggregation type %s)' % (self.__class__,    repr(self.data_area),
                                                                                     repr(self.data_field),
                                                                                     repr(self.data_item),
                                                                                     repr(self.aggregation_type))

    def get_data_area(self):
        return self.data_area

    def has_no_aggregation(self):
        return 0 == self.aggregation_type

    def has_formula_computed_di(self):
        return self.data_item.is_formula_computed

    def update_aggregation_type(self):
        dtp_record = ppms.search_record(411, [self.data_area, self.data_field], ['summary_type'])
        dtp_record.summary_type.set_value(5)
        dtp_record.save()

    @classmethod
    def get_list(cls, sql_packet):
        return sql_packet.select_or_log("""
            select
                di000306 data_area,
                di000307 data_field,
                di000213 data_item,
                di000288 aggregation_type
            from dt411 df
            where exists(
                select 1 from dt410 da
                where da.di000198 = df.di000306
                and da.di001720 = 1
            ) and exists (
                select 1 from dt412 di
                where df.di000213 = di.di000238
                and di.enabled = 1
                and exists (
                    select 1 from dt415 dt
                    where di.di000239 = dt.di000265
                    and dt.enabled = 1
                    and exists (
                        select 1 from dt420 ds
                        where dt.di000266 = ds.di000289
                        and ds.enabled = 1
                    )
                )
            ) order by data_area, data_field
            """)


class DataArea():
    """Data Area helper class.

    Data Area wrapper. Constructed from DataField instances partitioned by their DA ID. Immediately gathers DFs which
    have all DI dependencies available via DFs, i.e. used to be calculated in grouped DAs in server versions prior.
    """

    id = None
    formula_dfs = None
    all_dfs = None
    group_calculated_formula_dfs = None

    def __init__(self, id, formula_dfs, all_dfs):
        self.id = id
        self.formula_dfs = formula_dfs
        self.all_dfs = all_dfs
        self.all_df_dis = set(df.data_item for df in self.all_dfs)
        self.calc_group_calculated_formulae()

    def __equals__(self, other):
        return self.id == other.id

    def __repr__(self):
        return '%s (DA %s:\n%s)' % (self.__class__, repr(self.id),
                                                    self.repr_group_calculated_formula_dfs())

    def __str__(self):
        return 'DA {}'.format(self.id)

    def get_id(self):
        return self.id

    def repr_group_calculated_formula_dfs(self):
        retval = ''
        if self.group_calculated_formula_dfs:
            for df in self.group_calculated_formula_dfs:
                retval += '{}\n'.format(repr(df))
        return retval

    def has_group_calculated_formulae(self):
        """Check whether any DF in this DA matches our heuristics."""
        return not not self.group_calculated_formula_dfs

    def calc_group_calculated_formulae(self):
        """Gather DFs which have all DI dependencies available via DFs."""
        group_calculated_formula_dfs = list()
        for df in self.formula_dfs:
            if not self.df_has_missing_dependency(df):
                group_calculated_formula_dfs.append(df)
        if len(group_calculated_formula_dfs):
            self.group_calculated_formula_dfs = group_calculated_formula_dfs

    def df_has_missing_dependency(self, df):
        """Check whether DF in question is missing any DI dependencies wrt. those available via sibling DFs."""
        di = df.data_item
        if di.output_dependencies:
            for dependency in di.output_dependencies:
                if dependency not in self.all_df_dis:
                    return True
        return False

    @classmethod
    def get_affected_modules(self, sql_packet, data_areas):
        data_area_ids = (da.get_id() for da in data_areas)
        select_stmt = """
            select distinct di000970 module_id
            from dt406
            where di000969 in (\n  '{}')
            order by di000970
            """.format("""',\n  '""".join(data_area_ids))
        return [Module(x[0]) for x in sql_packet.select_or_log(select_stmt)]


class Module():
    """Module helper class.

    Wrap up some functionality to properly print module IDs and their title."""

    id = None
    name = None

    def __init__(self, id):
        dtp_record = ppms.search_record(405, [id], ['name'])
        self.id = id
        self.name = dtp_record.name.get_value()

    def __repr__(self):
        return '%s (MOD %s / %s)' % (self.__class__, repr(self.id),
                                                     self.name)

    def __str__(self):
        return """MOD {id} '{name}'""".format(id=self.id,
                                              name=self.name)


class ReplaceDependencyHeuristicsWithAggregationTypeNever(ChangeDTPPacket, SQLPacket):
    """Replaces "calculate in group" heuristics with aggregation type "never".

    Finds all DFs in group DAs which used to be subject to the "calculate formula inside the group" heuristics, and
    makes sure those are never aggregated.  The DFs in question are formula DIs where all DI dependencies are made
    available explicitly by means of sibling DFs inside the same DA.
    """

    version = 1
    run_on_startup = True
    done_after_success = True
    category = PacketCategory.CATEGORY_MANDATORY
    wi_number = 13831

    def get_dependencies(self):
        return (MinVersionDependency(version='39.4', version_type=VersionType.CUSTOMIZING),
                MaxVersionDependency(version='39.5.5', version_type=VersionType.CUSTOMIZING))

    def fix(self, runner):
        all_dfs_in_groups = DataField.get_list(self)
        all_dfs_in_groups = [DataField(df) for df in all_dfs_in_groups]
        formula_dfs_in_groups = (df for df in all_dfs_in_groups if df.has_no_aggregation() and df.has_formula_computed_di())

        formula_dfs_in_groups = partition(formula_dfs_in_groups, DataField.get_data_area)
        all_dfs_in_groups = partition(all_dfs_in_groups, DataField.get_data_area)

        data_areas = (DataArea(data_area, formula_dfs, all_dfs_in_groups[data_area]) for data_area, formula_dfs in formula_dfs_in_groups.items())
        data_areas = [da for da in data_areas if da.has_group_calculated_formulae()]

        if (len(data_areas)):
            log_messages = []
            data_areas.sort(key=DataArea.get_id)

            prev_state = ppms.db_get_auto_commit()
            ppms.db_set_auto_commit(False)

            try:
                for da in data_areas:
                    for df in da.group_calculated_formula_dfs:
                        log_messages.append("Update (DA {0}, DF {1}): change summary_type from {2} to 5"
                            .format(df.data_area, df.data_field, df.aggregation_type))
                        # do the update now
                        df.update_aggregation_type()

                ppms.db_commit()
            except ppms.DatabaseError:
                ppms.db_rollback()
                raise
            finally:
                ppms.db_set_auto_commit(prev_state)

            '''
            affected_das_text = 'Data Areas affected by this package:\n\n'
            for da in data_areas:
                affected_das_text += '\t{}\n'.format(str(da))
            self.log(affected_das_text)
            '''

            for log_message in log_messages:
                self.log(log_message)

            affected_modules_text = 'Modules affected by this package:\n\n'
            for mod in DataArea.get_affected_modules(self, data_areas):
                affected_modules_text += '\t{}\n'.format(str(mod))
            self.log(affected_modules_text)
        else:
            self.log('Found no DFs matching previous heuristics - nothing to do.')

        return self.success()
