# Generated by Django 5.2.9 on 2026-02-12 08:49
from collections import defaultdict
from contextlib import suppress

import django.contrib.postgres.constraints
import django.contrib.postgres.fields.ranges
from django.db import migrations, models
from psycopg.types.range import DateRange
from slugify import slugify
from datetime import datetime, timedelta
import re
from django.apps import apps as django_apps
from tqdm import tqdm

from wbcore.utils.date import find_closest_to_quarter
from wbcrm.models import Product as CRMProduct

pattern = re.compile(r".*?(.*\b[A-Z]{2}[A-Z0-9]{10}\b)\s+([A-Z][a-zA-Z ]+)")
isin_regex = "([A-Z]{2}[A-Z0-9]{9}[0-9]{1})"

def migrate_booking_entry(apps, schema_editor):
    BookingEntry = apps.get_model('wbaccounting', 'BookingEntry')
    BookingEntryType = apps.get_model('wbaccounting', 'BookingEntryType')
    Invoice = apps.get_model('wbaccounting', 'Invoice')
    Invoice.objects.filter(status="PAID", payment_date__isnull=True).update(payment_date=models.F("invoice_date"))

    def get_or_create_product(product_name):
        return CRMProduct.objects.get_or_create_internal_product(product_name)[0]

    product_map = {}
    if django_apps.is_installed("wbportfolio"):
        from wbportfolio.models import Product
        def get_or_create_product(product_name: str)-> str:
            if isins := re.findall(isin_regex, product_name):
                if isins[0] in product_map:
                    return product_map[isins[0]]
                with suppress(Product.DoesNotExist):
                    product = Product.objects.get(isin=isins[0])
                    if product.accounting_product:
                        product_map[isins[0]] = product.accounting_product
                        return product.accounting_product
            return get_or_create_product(product_name)


    def _parse_key(title: str):
        m = pattern.search(title)
        if m and (product_name := m.group(1)) and (booking_type := m.group(2)):
            return get_or_create_product(product_name), BookingEntryType.objects.get_or_create(key=slugify(booking_type), defaults={"title": booking_type})[0]
        else:
            return None, None

    objs = []

    # Loop over booking and extract the proper period from its parameters (prefer) or its booking date (otherwise)
    qs = BookingEntry.objects.all()
    for booking_entry in tqdm(qs, total=qs.count()):
        product, booking_type = _parse_key(booking_entry.title)
        if product and booking_type:
            booking_entry.product_id = product.id
            booking_entry.type_id = booking_type.id
            booking_entry.key = f'product-{product.id}-type-{booking_type.id}'
        else:
            booking_entry.key = slugify(booking_entry.title)

        lower = upper = None
        # if not parameters but booking is linked to an invoice who has entries with parameters, we choose one of this as parameters, otherwise we default to booking date
        if not booking_entry.parameters:
            if d := BookingEntry.objects.filter(invoice=booking_entry.invoice, parameters__from_date__isnull=False, parameters__to_date__isnull=False).first():
                lower, upper = datetime.strptime(d.parameters["from_date"], "%d.%m.%Y"), datetime.strptime(d.parameters["to_date"], "%d.%m.%Y")
            else:
                lower, upper = booking_entry.booking_date, booking_entry.booking_date
        else:
            lower, upper = datetime.strptime(booking_entry.parameters["from_date"], "%d.%m.%Y"), datetime.strptime(booking_entry.parameters["to_date"], "%d.%m.%Y")
        if upper < lower:
            lower, upper = upper, lower
        booking_entry.period = DateRange(lower, upper + timedelta(days=1)) # we have to add +1 days because the default bound for date range field is [) and postgres does not allow changing that.
        if not booking_entry.payment_date and booking_entry.invoice:
            booking_entry.payment_date = booking_entry.invoice.payment_date
        booking_entry.parameters = {}
        objs.append(booking_entry)
    print("done loading booking entries...")
    BookingEntry.objects.bulk_update(objs, ["period", "payment_date", "product_id", "type_id", "parameters", "key"], batch_size=1000)
    print("done saving booking entries...")


def clean_overlapping_period(apps, schema_editor):
    print("cleaning up overlapping periods...")
    BookingEntry = apps.get_model('wbaccounting', 'BookingEntry')
    keys = defaultdict(set)
    entries = BookingEntry.objects.filter(account__isnull=True, invoice__isnull=True)

    # we delete not invoiced booking entry longer than half a year
    entries.annotate(duration=models.F("period__endswith") - models.F("period__startswith")).filter(duration__gt=timedelta(days=180)).delete()
    # get all duplicates that shares the same overlapping periods. Doing a raw SQL with this is less straighforward so we resort to python
    for e in tqdm(entries, total=entries.count()):
        qs = BookingEntry.objects.filter(counterparty=e.counterparty, key=e.key, period__overlap=e.period, invoice__isnull=True, account__isnull=True).exclude(id=e.id)
        if qs.exists():
            keys[(e.key, e.counterparty.id)].add(e.period)
            keys[(e.key, e.counterparty.id)].update(set(qs.values_list("period", flat=True)))
    # for each duplicated keys and its associated overlapping period, we keep the period closest to a quarter and remove the reste. This is safe as this booking do not belong to any invoice yet
    for k, periods in keys.items():
        quarter_period = find_closest_to_quarter(periods)
        entries.filter(key=k[0], counterparty_id=k[1], period__overlap=(quarter_period.lower, quarter_period.upper)).exclude(period=(quarter_period.lower, quarter_period.upper)).delete()


SQL1 = """
WITH ranked AS (
    SELECT
        id,
        ROW_NUMBER() OVER (
            PARTITION BY period, counterparty_id, key
            ORDER BY id DESC
        ) AS rn
    FROM wbaccounting_bookingentry
    WHERE account_id IS NULL AND invoice_id IS NULL
)
DELETE FROM wbaccounting_bookingentry b
USING ranked r
WHERE b.id = r.id
  AND r.rn > 1;
"""

SQL2 = """
WITH ranked AS (
    SELECT
        id,
        ROW_NUMBER() OVER (
            PARTITION BY key, period, counterparty_id, key, invoice_id
            ORDER BY id DESC
        ) AS rn
    FROM wbaccounting_bookingentry
    WHERE invoice_id IS NOT NULL AND account_id IS NULL
)
DELETE FROM wbaccounting_bookingentry b
USING ranked r
WHERE b.id = r.id
  AND r.rn > 1;
"""


BASE_DEPENDENCIES = [
        ('currency', '0002_alter_currencyfxrates_unique_together_and_more'),
        ('directory', '0015_alter_emailcontact_address_and_more'),
        ('wbaccounting', '0013_vat_alter_entryaccountinginformation_vat_vatrate'),
        ('wbcrm', '0020_alter_product_unique_together_product_unique_product'),
]
class Migration(migrations.Migration):

    dependencies = BASE_DEPENDENCIES + [("wbportfolio", "0099_product_accounting_product")] if django_apps.is_installed("wbportfolio") else BASE_DEPENDENCIES

    operations = [
        migrations.CreateModel(
            name='BookingEntryType',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('key', models.CharField(db_index=True, max_length=255, unique=True, verbose_name='Key')),
                ('title', models.CharField(max_length=255, verbose_name='Title')),
            ],
            options={'verbose_name': 'Booking Type', 'verbose_name_plural': 'Bookings Type'}
        ),
        migrations.AddField(
            model_name='bookingentry',
            name='key',
            field=models.CharField(default=None, blank=True, null=True, max_length=255, verbose_name='Key'),
            preserve_default=False,
        ),
        migrations.AddField(
            model_name='bookingentry',
            name='account',
            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='booking_entries', to='wbcrm.account', verbose_name='Account'),
        ),
        migrations.AddField(
            model_name='bookingentry',
            name='period',
            field=django.contrib.postgres.fields.ranges.DateRangeField(blank=True, null=True, verbose_name='Period'),
            preserve_default=False,
        ),
        migrations.AddField(
            model_name='bookingentry',
            name='product',
            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='booking_entries', to='wbcrm.product', verbose_name='Product'),
            preserve_default=False,
        ),
        migrations.AddField(
            model_name='bookingentry',
            name='quantity',
            field=models.IntegerField(blank=True, null=True, verbose_name='Quantity'),
        ),
        migrations.AddField(
            model_name='bookingentry',
            name='type',
            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
                                    related_name='booking_entries', to='wbaccounting.bookingentrytype',
                                    verbose_name='Product'),
            preserve_default=False,
        ),
        migrations.AddField(
            model_name='bookingentry',
            name='initial_booking_entry',
            field=models.ForeignKey(blank=True,
                                    help_text='If set, contains the booking entry already invoiced wich this entry values should override.',
                                    null=True, on_delete=django.db.models.deletion.SET_NULL,
                                    related_name='related_entries', to='wbaccounting.bookingentry',
                                    verbose_name='Initial Booking Entry'),
        ),
        migrations.RunPython(migrate_booking_entry),
        migrations.RunSQL(SQL1),
        migrations.RunSQL(SQL2),
        migrations.RunPython(clean_overlapping_period)
    ]
