from datetime import datetime
from decimal import Decimal, DecimalException, InvalidOperation

from import_export import resources

from hordak.models import StatementLine, ToField
from hordak.utilities.statement_import import DATE_FORMATS


class StatementLineResource(resources.ModelResource):
    class Meta:
        model = StatementLine
        fields = ("date", "amount", "description")
        import_id_fields = ()

    def __init__(self, date_format, statement_import):
        self.date_format = date_format
        self.statement_import = statement_import

    def before_import(self, dataset, *args, **kwargs):
        def _strip(s):
            return s.strip() if isinstance(s, str) else s

        # Remove whitespace
        for index, values in enumerate(dataset):
            dataset[index] = tuple(map(_strip, values))

        # We're going to need this to check for duplicates (because
        # there could be multiple identical transactions)
        self.dataset = dataset
        similar_totals = [0] * len(self.dataset)

        for i, row in enumerate(dataset):
            num_similar = self._get_num_similar_rows(row, until=i)
            similar_totals[i] = num_similar

        # Add a new 'similar_total' column. This is a integer of how many
        # identical rows precede this one.
        self.dataset.append_col(similar_totals, header="similar_total")

    def before_save_instance(self, instance, *args, **kwargs):
        # We need to record this statement line against the parent statement import
        # instance passed to the constructor
        instance.statement_import = self.statement_import

    def get_instance(self, instance_loader, row):
        # We never update, we either create or skip
        return None

    def init_instance(self, row=None):
        # Attach the row to the instance as we'll need it in skip_row()
        instance = super(StatementLineResource, self).init_instance(row)
        instance._row = row
        return instance

    def skip_row(self, instance, original, *args, **kwargs):
        # Skip this row if the database already contains the requsite number of
        # rows identical to this one.
        return instance._row["similar_total"] < self._get_num_similar_objects(instance)

    def _get_num_similar_objects(self, obj):
        """Get any statement lines which would be considered a duplicate of obj"""
        return StatementLine.objects.filter(
            date=obj.date, amount=obj.amount, description=obj.description
        ).count()

    def _get_num_similar_rows(self, row, until=None):
        """Get the number of rows similar to row which precede the index `until`"""
        return len(list(filter(lambda r: row == r, self.dataset[:until])))

    def import_obj(self, obj, data, dry_run, *args, **kwargs):
        return self.import_instance(obj, data, *args, dry_run=dry_run, **kwargs)

    def import_instance(self, instance, row, *args, **kwargs):
        F = ToField
        use_dual_amounts = F.amount_out.value in row and F.amount_in.value in row

        if F.date.value not in row:
            raise ValueError("No date column found")

        try:
            date = datetime.strptime(row[F.date.value], self.date_format).date()
        except ValueError:
            raise ValueError(
                "Invalid value for date. Expected {}".format(
                    dict(DATE_FORMATS)[self.date_format]
                )
            )

        description = row[F.description.value]

        # Do we have in/out columns, or just one amount column?
        if use_dual_amounts:
            amount_out = row[F.amount_out.value]
            amount_in = row[F.amount_in.value]

            if amount_in and amount_out:
                raise ValueError("Values found for both Amount In and Amount Out")
            if not amount_in and not amount_out:
                raise ValueError("Value required for either Amount In or Amount Out")

            if amount_out:
                try:
                    amount = abs(Decimal(amount_out)) * -1
                except DecimalException:
                    raise ValueError("Invalid value found for Amount Out")
            else:
                try:
                    amount = abs(Decimal(amount_in))
                except DecimalException:
                    raise ValueError("Invalid value found for Amount In")
        else:
            if F.amount.value not in row:
                raise ValueError("No amount column found")
            if not row[F.amount.value]:
                raise ValueError("No value found for amount")
            try:
                amount = Decimal(row[F.amount.value])
            except InvalidOperation:
                raise DecimalException("Invalid value found for Amount")

        if amount == Decimal("0"):
            raise ValueError("Amount of zero not allowed")

        row = dict(date=date, amount=amount, description=description)
        try:
            return super(StatementLineResource, self).import_instance(
                instance, row, *args, **kwargs
            )
        except AttributeError:  # django-import-export < 4.0.0
            return super(StatementLineResource, self).import_obj(
                instance, row, *args, **kwargs
            )
