"""
Clean reimport: Delete all migration data and reimport from legacy.
Run inside erpnext-backend-1:
  /home/frappe/frappe-bench/env/bin/python /home/frappe/frappe-bench/clean_reimport.py

Migrates invoices, payments, GL entries, PLE entries, and credit allocations
from legacy MariaDB (gestionclient) to ERPNext PostgreSQL.

Naming: SINV-{legacy_id} for invoices, PE-{legacy_id} for payments.
Post-migration naming will use SINV-YYYY-NNNNN format to avoid collisions.
"""
import frappe
import pymysql
import os
import time
import re
import sys

sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 1)  # unbuffered

os.chdir("/home/frappe/frappe-bench/sites")
frappe.init(site="erp.gigafibre.ca", sites_path=".")
frappe.connect()
print("Connected:", frappe.local.site)

T_TOTAL = time.time()

# ═══════════════════════════════════════════════════════════════
# PHASE 0: CLEANUP — Delete all existing accounting data
# ═══════════════════════════════════════════════════════════════
print("\n" + "="*60)
print("PHASE 0: CLEANUP")
print("="*60)
t0 = time.time()

tables_to_clear = [
    ("tabGL Entry", ""),
    ("tabPayment Ledger Entry", ""),
    ("tabSales Taxes and Charges", ""),
    ("tabPayment Entry Reference", ""),
    ("tabPayment Entry", ""),
    ("tabSales Invoice Payment", ""),
    ("tabSales Invoice Item", ""),
    ("tabSales Invoice", ""),
]

for table, extra in tables_to_clear:
    c = frappe.db.sql('SELECT COUNT(*) FROM "{}"'.format(table))[0][0]
    if c > 0:
        frappe.db.sql('DELETE FROM "{}"'.format(table))
        frappe.db.commit()
        print("  Deleted {} rows from {}".format(c, table))
    else:
        print("  {} already empty".format(table))

print("  Cleanup done [{:.0f}s]".format(time.time() - t0))

# ═══════════════════════════════════════════════════════════════
# PHASE 1: LOAD ALL LEGACY DATA
# ═══════════════════════════════════════════════════════════════
print("\n" + "="*60)
print("PHASE 1: LOAD LEGACY DATA")
print("="*60)
t0 = time.time()

legacy = pymysql.connect(
    host="legacy-db", user="facturation", password="*******",
    database="gestionclient", cursorclass=pymysql.cursors.DictCursor
)

# 1a: Load invoices
print("\n--- 1a: Invoices ---")
with legacy.cursor() as cur:
    cur.execute("""
        SELECT i.id, i.account_id, i.total_amt, i.billed_amt,
               FROM_UNIXTIME(i.date_orig) as date_created,
               i.date_orig, i.notes
        FROM invoice i
        WHERE i.date_orig > 0
        ORDER BY i.id
    """)
    invoices = cur.fetchall()
print("  Loaded {} invoices".format(len(invoices)))

# 1a2: Load SKU → GL account mapping from product → product_cat
print("\n--- 1a2: SKU → GL account mapping ---")
with legacy.cursor() as cur:
    cur.execute("""
        SELECT p.sku, pc.num_compte
        FROM product p JOIN product_cat pc ON p.category = pc.id
        WHERE p.sku IS NOT NULL AND pc.num_compte IS NOT NULL
    """)
    sku_to_gl_num = {}
    for r in cur.fetchall():
        if r['num_compte']:
            sku_to_gl_num[r['sku']] = str(int(r['num_compte']))
print("  Loaded {} SKU→GL number mappings".format(len(sku_to_gl_num)))

# Build GL account number → ERPNext account name
gl_by_number = {}
acct_rows = frappe.db.sql("""
    SELECT account_number, name FROM "tabAccount"
    WHERE root_type IN ('Income', 'Expense') AND company = 'TARGO' AND is_group = 0
      AND account_number IS NOT NULL AND account_number != ''
""", as_dict=True)
for a in acct_rows:
    gl_by_number[a['account_number']] = a['name']
print("  {} numbered GL accounts in ERPNext".format(len(gl_by_number)))

# Combined: SKU → ERPNext income account name
DEFAULT_INCOME = "Autres produits d'exploitation - T"
sku_to_income = {}
for sku, num in sku_to_gl_num.items():
    if num in gl_by_number:
        sku_to_income[sku] = gl_by_number[num]
print("  {} SKUs mapped to named GL accounts".format(len(sku_to_income)))

# 1b: Load invoice items (with SKU for GL mapping)
print("\n--- 1b: Invoice items (with SKU) ---")
with legacy.cursor() as cur:
    invoice_ids = [str(i['id']) for i in invoices]
    all_items = {}
    chunk = 10000
    for idx in range(0, len(invoice_ids), chunk):
        batch = invoice_ids[idx:idx+chunk]
        cur.execute("""
            SELECT ii.invoice_id, ii.product_name, ii.quantity, ii.unitary_price,
                   (ii.quantity * ii.unitary_price) as total, ii.sku
            FROM invoice_item ii
            WHERE ii.invoice_id IN ({})
            ORDER BY ii.id
        """.format(",".join(batch)))
        for it in cur.fetchall():
            iid = it['invoice_id']
            if iid not in all_items:
                all_items[iid] = []
            all_items[iid].append(it)
    total_items = sum(len(v) for v in all_items.values())
print("  Loaded {} items for {} invoices".format(total_items, len(all_items)))

# 1c: Load invoice taxes
# ⚠️ Keep BOTH the sum of TPS/TVQ amounts AND the maximum rate seen per tax-type
#    for each invoice. Historical invoices (< 2013) have TVQ @ 8.925% or 9.5%,
#    not 9.975%. We preserve the actual rate so descriptions don't lie.
#    Using `+=` (not `=`) because legacy occasionally splits a single tax into
#    two rows (e.g. adjustment + tax) and we want the grand total.
print("\n--- 1c: Invoice taxes ---")
with legacy.cursor() as cur:
    all_taxes = {}
    for idx in range(0, len(invoice_ids), chunk):
        batch = invoice_ids[idx:idx+chunk]
        cur.execute("""
            SELECT invoice_id, tax_name, tax_rate, amount
            FROM invoice_tax
            WHERE invoice_id IN ({})
        """.format(",".join(batch)))
        for t in cur.fetchall():
            iid = t['invoice_id']
            if iid not in all_taxes:
                all_taxes[iid] = {'tps': 0.0, 'tvq': 0.0, 'tps_rate': 5.0, 'tvq_rate': 9.975}
            name = (t['tax_name'] or '').upper()
            amt = float(t['amount'] or 0)
            rate = float(t['tax_rate'] or 0) * 100  # legacy stores 0.05 → we want 5.0
            if 'TPS' in name or 'GST' in name:
                all_taxes[iid]['tps'] += amt
                if rate > 0:
                    all_taxes[iid]['tps_rate'] = rate
            elif 'TVQ' in name or 'QST' in name or 'PST' in name:
                all_taxes[iid]['tvq'] += amt
                if rate > 0:
                    all_taxes[iid]['tvq_rate'] = rate
print("  Loaded {} tax records".format(len(all_taxes)))

# 1d: Load payments
print("\n--- 1d: Payments ---")
with legacy.cursor() as cur:
    cur.execute("""
        SELECT p.id, p.account_id, p.amount, p.type, p.memo, p.reference,
               FROM_UNIXTIME(p.date_orig) as date_created,
               p.date_orig
        FROM payment p
        WHERE p.date_orig > 0
          AND p.type NOT IN ('credit', 'reversement', 'credit targo')
        ORDER BY p.id
    """)
    payments_raw = cur.fetchall()

# Deduplicate payments by (account_id, reference) — legacy double-submits
seen_refs = set()
payments = []
dedup_count = 0
for p in payments_raw:
    ref = p.get('reference') or ''
    if ref:
        key = (p['account_id'], ref)
        if key in seen_refs:
            dedup_count += 1
            continue
        seen_refs.add(key)
    payments.append(p)
print("  Loaded {} payments, deduplicated {} (by reference)".format(len(payments), dedup_count))

# 1e: Load payment items (allocations)
print("\n--- 1e: Payment allocations ---")
with legacy.cursor() as cur:
    payment_ids = [str(p['id']) for p in payments]
    all_payment_items = {}
    for idx in range(0, len(payment_ids), chunk):
        batch = payment_ids[idx:idx+chunk]
        cur.execute("""
            SELECT pi.payment_id, pi.invoice_id, pi.amount
            FROM payment_item pi
            WHERE pi.payment_id IN ({})
              AND pi.amount != 0
        """.format(",".join(batch)))
        for pi in cur.fetchall():
            pid = pi['payment_id']
            if pid not in all_payment_items:
                all_payment_items[pid] = []
            all_payment_items[pid].append(pi)
    total_pi = sum(len(v) for v in all_payment_items.values())
print("  Loaded {} allocations for {} payments".format(total_pi, len(all_payment_items)))

# 1f: Load credit allocations (credit type payments)
print("\n--- 1f: Credit allocations ---")
with legacy.cursor() as cur:
    cur.execute("""
        SELECT p.id as payment_id, p.memo, p.account_id,
               pi.invoice_id as target_invoice_id, pi.amount as allocated_amount
        FROM payment p
        JOIN payment_item pi ON pi.payment_id = p.id
        WHERE p.type = 'credit'
          AND p.date_orig > 0
          AND pi.amount != 0
    """)
    credit_allocs_raw = cur.fetchall()

credit_allocs = []
for row in credit_allocs_raw:
    memo = row.get("memo") or ""
    match = re.search(r'#(\d+)', memo)
    if match:
        credit_allocs.append({
            "source_invoice_id": match.group(1),
            "target_invoice_id": str(row["target_invoice_id"]),
            "amount": float(row["allocated_amount"]),
        })
print("  Loaded {} credit allocations".format(len(credit_allocs)))

# 1g: Load reversement allocations (reversement payments link credit → target via memo + payment_item)
print("\n--- 1g: Reversement allocations ---")
with legacy.cursor() as cur:
    cur.execute("""
        SELECT p.memo, pi.invoice_id as target_invoice_id
        FROM payment p
        JOIN payment_item pi ON pi.payment_id = p.id
        WHERE p.type = 'reversement'
          AND p.date_orig > 0
          AND p.memo LIKE 'create by invoice #%'
    """)
    rev_allocs_raw = cur.fetchall()

reversement_allocs = []
for row in rev_allocs_raw:
    memo = row.get("memo") or ""
    # memo format: "create by invoice #CREDIT for invoice #TARGET"
    m = re.search(r'create by invoice #(\d+)', memo)
    if m:
        reversement_allocs.append({
            "source_invoice_id": m.group(1),
            "target_invoice_id": str(row["target_invoice_id"]),
        })
print("  Loaded {} reversement allocations".format(len(reversement_allocs)))

legacy.close()
print("\n  All legacy data loaded [{:.0f}s]".format(time.time() - t0))

# ═══════════════════════════════════════════════════════════════
# PHASE 2: MAP CUSTOMERS
# ═══════════════════════════════════════════════════════════════
print("\n" + "="*60)
print("PHASE 2: MAP CUSTOMERS")
print("="*60)
t0 = time.time()

cust_rows = frappe.db.sql("""
    SELECT name, legacy_account_id FROM "tabCustomer"
    WHERE legacy_account_id IS NOT NULL
""", as_dict=True)
acct_to_cust = {}
for c in cust_rows:
    acct_to_cust[str(c["legacy_account_id"])] = c["name"]
print("  Mapped {} customers".format(len(acct_to_cust)))

# Check for unmapped invoices
unmapped_invoices = []
for inv in invoices:
    if str(inv['account_id']) not in acct_to_cust:
        unmapped_invoices.append(inv['id'])
print("  Invoices with unmapped customers: {}".format(len(unmapped_invoices)))
if unmapped_invoices:
    print("  First 10 unmapped: {}".format(unmapped_invoices[:10]))

print("  [{:.0f}s]".format(time.time() - t0))

# ═══════════════════════════════════════════════════════════════
# PHASE 3: INSERT SALES INVOICES
# ═══════════════════════════════════════════════════════════════
print("\n" + "="*60)
print("PHASE 3: INSERT SALES INVOICES")
print("="*60)
t0 = time.time()

# Build invoice ID map for credit allocations
inv_id_to_sinv = {}  # legacy_id -> SINV name

# Returns are identified by negative total_amt
# Credit links come from payment type='credit' memo field (loaded in credit_allocs)

insert_count = 0
skip_count = 0
CHUNK = 2000

for idx in range(0, len(invoices), CHUNK):
    batch = invoices[idx:idx+CHUNK]
    si_values = []
    si_item_values = []
    si_tax_values = []

    for inv in batch:
        cust = acct_to_cust.get(str(inv['account_id']))
        if not cust:
            skip_count += 1
            continue

        sinv_name = "SINV-{}".format(inv['id'])
        inv_id_to_sinv[str(inv['id'])] = sinv_name

        total_amt = float(inv['total_amt'])
        is_return = 1 if total_amt < 0 else 0
        posting_date = str(inv['date_created'])[:10]

        # Tax split — authoritative from legacy, with safe fallback.
        tax_info = all_taxes.get(inv['id'])
        if tax_info:
            tps = round(float(tax_info['tps']), 2)
            tvq = round(float(tax_info['tvq']), 2)
            tps_rate = float(tax_info['tps_rate'])
            tvq_rate = float(tax_info['tvq_rate'])
        else:
            # No legacy tax row → estimate from total_amt assuming 14.975% combined
            # (5% TPS + 9.975% TVQ, the modern Quebec rate since 2013).
            tps_rate = 5.0
            tvq_rate = 9.975
            if total_amt != 0:
                net = round(total_amt / 1.14975, 2)
                tps = round(net * 0.05, 2)
                tvq = round(net * 0.09975, 2)
            else:
                net = 0.0
                tps = 0.0
                tvq = 0.0

        net_total = round(total_amt - tps - tvq, 2)

        # ⚠️ Sanity check: net + tps + tvq MUST equal total_amt (grand_total).
        #    If drift > 1¢, ERPNext's later validation will throw
        #    "Total (including taxes) and Grand Total do not match". Detect here.
        _drift = round(net_total + tps + tvq - total_amt, 2)
        if abs(_drift) > 0.01:
            print("  WARN invoice_id={} tax drift {:+.2f} (net={} tps={} tvq={} gt={})".format(
                inv['id'], _drift, net_total, tps, tvq, total_amt))

        # return_against — set later in phase for credit allocations
        return_against = ""

        si_values.append(
            "('{name}', 'Administrator', NOW(), NOW(), 'Administrator', "
            "0, '{customer}', '{customer_name}', "
            "'{posting_date}', '{posting_date}', "
            "'{company}', '{currency}', 1, "
            "{grand_total}, {grand_total}, "
            "{net_total}, {net_total}, {net_total}, {net_total}, "
            "{total_taxes}, "
            "{is_return}, '{return_against}', "
            "{outstanding}, "
            "'{debit_to}', 'Customer', "
            "{legacy_id}, "
            "'Draft', 1)".format(
                name=sinv_name,
                customer=cust.replace("'", "''"),
                customer_name=cust.replace("'", "''"),
                posting_date=posting_date,
                company="TARGO",
                currency="CAD",
                grand_total=total_amt,
                net_total=net_total,
                total_taxes=round(tps + tvq, 2),
                is_return=is_return,
                return_against=return_against,
                outstanding=total_amt,
                debit_to="Comptes clients - T",
                legacy_id=inv['id'],
            )
        )

        # Invoice items
        items = all_items.get(inv['id'], [])
        if not items:
            # Create a single catch-all item
            items = [{"product_name": "Services", "quantity": 1, "unitary_price": net_total, "total": net_total}]

        for item_idx, it in enumerate(items):
            item_name = "SII-{}-{}".format(inv['id'], item_idx)
            item_total = float(it.get('total', 0) or 0)
            item_qty = float(it.get('quantity', 1) or 1)
            item_rate = float(it.get('unitary_price', 0) or 0)
            desc = str(it.get('product_name', 'Services') or 'Services').replace("'", "''")[:140]

            # Map to correct GL account via SKU → product_cat → num_compte
            item_sku = it.get('sku') or ''
            income_account = sku_to_income.get(item_sku, DEFAULT_INCOME).replace("'", "''")

            si_item_values.append(
                "('{name}', 'Administrator', NOW(), NOW(), 'Administrator', "
                "'{parent}', 'Sales Invoice', 'items', {idx}, "
                "'{item_code}', '{item_name}', '{description}', '{uom}', "
                "{qty}, {rate}, {amount}, {amount}, {amount}, {amount}, "
                "'{income_account}', 0)".format(
                    name=item_name,
                    parent=sinv_name,
                    idx=item_idx + 1,
                    item_code="SVC",
                    item_name=desc[:140],
                    description=desc[:140],
                    uom="Nos",
                    qty=item_qty,
                    rate=item_rate,
                    amount=item_total,
                    income_account=income_account,
                )
            )

        # Tax rows — each row carries the REAL rate from legacy so historical
        # invoices (pre-2013 TVQ at 8.925% / 9.5%) keep accurate descriptions.
        # Only emit non-zero rows: a zero-amount TPS or TVQ line would render
        # "TPS 5% $0.00" on the PDF which is cosmetically wrong.
        if tps != 0:
            si_tax_values.append(
                "('{name}', 'Administrator', NOW(), NOW(), 'Administrator', "
                "'{parent}', 'Sales Invoice', 'taxes', 1, "
                "'On Net Total', '2300 - TPS perçue - T', "
                "'TPS {rate}% (#834975559RT0001)', {rate}, "
                "{tps}, {tps}, {running1}, "
                "'', 0)".format(
                    name="stc-tps-{}".format(inv['id']),
                    parent=sinv_name,
                    rate=("%g" % tps_rate),  # %g strips trailing zeros → "5" not "5.0"
                    tps=tps,
                    running1=round(net_total + tps, 2),
                )
            )
        if tvq != 0:
            si_tax_values.append(
                "('{name}', 'Administrator', NOW(), NOW(), 'Administrator', "
                "'{parent}', 'Sales Invoice', 'taxes', 2, "
                "'On Net Total', '2350 - TVQ perçue - T', "
                "'TVQ {rate}% (#1213765929TQ0001)', {rate}, "
                "{tvq}, {tvq}, {running2}, "
                "'', 0)".format(
                    name="stc-tvq-{}".format(inv['id']),
                    parent=sinv_name,
                    rate=("%g" % tvq_rate),
                    tvq=tvq,
                    running2=total_amt,
                )
            )

        insert_count += 1

    # Bulk INSERT Sales Invoices
    if si_values:
        frappe.db.sql("""
            INSERT INTO "tabSales Invoice" (
                name, owner, creation, modified, modified_by,
                docstatus, customer, customer_name,
                posting_date, due_date,
                company, currency, conversion_rate,
                grand_total, base_grand_total,
                net_total, base_net_total, total, base_total,
                total_taxes_and_charges,
                is_return, return_against,
                outstanding_amount,
                debit_to, party_account_currency,
                legacy_invoice_id,
                status, set_posting_time
            ) VALUES {}
        """.format(",".join(si_values)))

    # Bulk INSERT Invoice Items
    if si_item_values:
        frappe.db.sql("""
            INSERT INTO "tabSales Invoice Item" (
                name, owner, creation, modified, modified_by,
                parent, parenttype, parentfield, idx,
                item_code, item_name, description, uom,
                qty, rate, amount, base_amount, net_amount, base_net_amount,
                income_account, docstatus
            ) VALUES {}
        """.format(",".join(si_item_values)))

    # Bulk INSERT Tax rows
    if si_tax_values:
        frappe.db.sql("""
            INSERT INTO "tabSales Taxes and Charges" (
                name, owner, creation, modified, modified_by,
                parent, parenttype, parentfield, idx,
                charge_type, account_head,
                description, rate,
                tax_amount, base_tax_amount, total,
                cost_center, docstatus
            ) VALUES {}
        """.format(",".join(si_tax_values)))

    frappe.db.commit()
    if (idx + CHUNK) % 20000 == 0 or idx + CHUNK >= len(invoices):
        print("  Inserted {}/{} invoices...".format(min(idx + CHUNK, len(invoices)), len(invoices)))

print("  Inserted {} invoices, skipped {} (no customer) [{:.0f}s]".format(insert_count, skip_count, time.time() - t0))

# ═══════════════════════════════════════════════════════════════
# PHASE 4: SET return_against AND UPDATE docstatus
# ═══════════════════════════════════════════════════════════════
print("\n" + "="*60)
print("PHASE 4: SET return_against + SUBMIT ALL")
print("="*60)
t0 = time.time()

# Set return_against from credit_allocs (credit payments) — batch via temp table
credit_alloc_pairs = []
credit_alloc_source_ids = set()
for alloc in credit_allocs:
    source_sinv = inv_id_to_sinv.get(alloc["source_invoice_id"])
    target_sinv = inv_id_to_sinv.get(alloc["target_invoice_id"])
    if source_sinv and target_sinv and alloc["source_invoice_id"] not in credit_alloc_source_ids:
        credit_alloc_pairs.append((source_sinv, target_sinv))
        credit_alloc_source_ids.add(alloc["source_invoice_id"])

if credit_alloc_pairs:
    frappe.db.sql("DROP TABLE IF EXISTS _tmp_return_against")
    frappe.db.commit()
    frappe.db.sql("CREATE TABLE _tmp_return_against (source_sinv VARCHAR(140), target_sinv VARCHAR(140))")
    frappe.db.commit()
    for i in range(0, len(credit_alloc_pairs), 5000):
        batch = credit_alloc_pairs[i:i+5000]
        values = ",".join(["('{}', '{}')".format(s, t) for s, t in batch])
        frappe.db.sql("INSERT INTO _tmp_return_against VALUES {}".format(values))
    frappe.db.sql("""
        UPDATE "tabSales Invoice" si
        SET return_against = ra.target_sinv
        FROM _tmp_return_against ra
        WHERE si.name = ra.source_sinv
          AND si.is_return = 1
          AND (si.return_against IS NULL OR si.return_against = '')
    """)
    frappe.db.commit()
    frappe.db.sql("DROP TABLE IF EXISTS _tmp_return_against")
    frappe.db.commit()
print("  Set return_against from credit allocs on {} invoices".format(len(credit_alloc_pairs)))

# Set return_against from reversal notes (Renversement de la facture #NNN)
# These are SEPARATE from credit allocs — zero overlap
reversal_matches = []  # (credit_sinv, target_sinv)
for inv in invoices:
    if float(inv['total_amt']) >= 0:
        continue
    notes = inv.get('notes') or ''
    if 'Renversement de la facture #' not in notes:
        continue
    m = re.search(r'#(\d+)', notes)
    if not m:
        continue
    target_leg_id = m.group(1)
    source_sinv = inv_id_to_sinv.get(str(inv['id']))
    target_sinv = inv_id_to_sinv.get(target_leg_id)
    if source_sinv and target_sinv and str(inv['id']) not in credit_alloc_source_ids:
        reversal_matches.append((source_sinv, target_sinv))

if reversal_matches:
    frappe.db.sql("DROP TABLE IF EXISTS _tmp_return_against")
    frappe.db.commit()
    frappe.db.sql("CREATE TABLE _tmp_return_against (source_sinv VARCHAR(140), target_sinv VARCHAR(140))")
    frappe.db.commit()
    for i in range(0, len(reversal_matches), 5000):
        batch = reversal_matches[i:i+5000]
        values = ",".join(["('{}', '{}')".format(s, t) for s, t in batch])
        frappe.db.sql("INSERT INTO _tmp_return_against VALUES {}".format(values))
    frappe.db.sql("""
        UPDATE "tabSales Invoice" si
        SET return_against = ra.target_sinv
        FROM _tmp_return_against ra
        WHERE si.name = ra.source_sinv
          AND si.is_return = 1
          AND (si.return_against IS NULL OR si.return_against = '')
    """)
    frappe.db.commit()
    frappe.db.sql("DROP TABLE IF EXISTS _tmp_return_against")
    frappe.db.commit()
print("  Set return_against from reversal notes on {} invoices".format(len(reversal_matches)))

# Set return_against from reversement payment memos (3rd mechanism)
# memo: "create by invoice #CREDIT for invoice #TARGET", payment_item.invoice_id = TARGET
rev_notes_source_ids = set(str(inv['id']) for inv in invoices
    if float(inv['total_amt']) < 0 and (inv.get('notes') or '').startswith('Renversement'))
already_linked = credit_alloc_source_ids | rev_notes_source_ids

reversement_matches = []
for alloc in reversement_allocs:
    if alloc["source_invoice_id"] in already_linked:
        continue
    source_sinv = inv_id_to_sinv.get(alloc["source_invoice_id"])
    target_sinv = inv_id_to_sinv.get(alloc["target_invoice_id"])
    if source_sinv and target_sinv:
        reversement_matches.append((source_sinv, target_sinv))
        already_linked.add(alloc["source_invoice_id"])

if reversement_matches:
    frappe.db.sql("DROP TABLE IF EXISTS _tmp_return_against")
    frappe.db.commit()
    frappe.db.sql("CREATE TABLE _tmp_return_against (source_sinv VARCHAR(140), target_sinv VARCHAR(140))")
    frappe.db.commit()
    for i in range(0, len(reversement_matches), 5000):
        batch = reversement_matches[i:i+5000]
        values = ",".join(["('{}', '{}')".format(s, t) for s, t in batch])
        frappe.db.sql("INSERT INTO _tmp_return_against VALUES {}".format(values))
    frappe.db.sql("""
        UPDATE "tabSales Invoice" si
        SET return_against = ra.target_sinv
        FROM _tmp_return_against ra
        WHERE si.name = ra.source_sinv
          AND si.is_return = 1
          AND (si.return_against IS NULL OR si.return_against = '')
    """)
    frappe.db.commit()
    frappe.db.sql("DROP TABLE IF EXISTS _tmp_return_against")
    frappe.db.commit()
print("  Set return_against from reversement memos on {} invoices".format(len(reversement_matches)))

# Submit all invoices
frappe.db.sql('UPDATE "tabSales Invoice" SET docstatus = 1')
frappe.db.sql('UPDATE "tabSales Invoice Item" SET docstatus = 1')
frappe.db.sql('UPDATE "tabSales Taxes and Charges" SET docstatus = 1')
frappe.db.commit()
sinv_count = frappe.db.sql('SELECT COUNT(*) FROM "tabSales Invoice" WHERE docstatus = 1')[0][0]
print("  Submitted {} invoices [{:.0f}s]".format(sinv_count, time.time() - t0))

# ═══════════════════════════════════════════════════════════════
# PHASE 5: INSERT PAYMENT ENTRIES
# ═══════════════════════════════════════════════════════════════
print("\n" + "="*60)
print("PHASE 5: INSERT PAYMENT ENTRIES")
print("="*60)
t0 = time.time()

pe_count = 0
per_count = 0

for idx in range(0, len(payments), CHUNK):
    batch = payments[idx:idx+CHUNK]
    pe_values = []
    per_values = []

    for pmt in batch:
        cust = acct_to_cust.get(str(pmt['account_id']))
        if not cust:
            continue

        pe_name = "PE-{}".format(pmt['id'])
        amount = float(pmt['amount'])
        posting_date = str(pmt['date_created'])[:10]
        pmt_type = pmt.get('type', 'payment')
        memo = str(pmt.get('memo', '') or '')[:140].replace("'", "''").replace("\\", "\\\\")

        pe_values.append(
            "('{name}', 'Administrator', NOW(), NOW(), 'Administrator', "
            "1, '{posting_date}', 'TARGO', "
            "'Receive', 'Customer', '{party}', '{party_name}', "
            "'Comptes clients - T', 'Banque - T', "
            "{paid}, {paid}, {paid}, {received}, "
            "1, 'CAD', 'CAD', "
            "'{remarks}', "
            "'Submitted', {legacy_id})".format(
                name=pe_name,
                posting_date=posting_date,
                party=cust.replace("'", "''"),
                party_name=cust.replace("'", "''"),
                paid=amount,
                received=amount,
                remarks=memo,
                legacy_id=pmt['id'],
            )
        )
        pe_count += 1

        # Payment references (allocations to invoices)
        items = all_payment_items.get(pmt['id'], [])
        for pi_idx, pi in enumerate(items):
            target_sinv = inv_id_to_sinv.get(str(pi['invoice_id']))
            if not target_sinv:
                continue
            per_name = "PER-{}-{}".format(pmt['id'], pi_idx)
            alloc_amt = float(pi['amount'])

            per_values.append(
                "('{name}', 'Administrator', NOW(), NOW(), 'Administrator', "
                "'{parent}', 'Payment Entry', 'references', {idx}, "
                "'Sales Invoice', '{ref_name}', "
                "{allocated}, "
                "1, 1)".format(
                    name=per_name,
                    parent=pe_name,
                    idx=pi_idx + 1,
                    ref_name=target_sinv,
                    allocated=alloc_amt,
                )
            )
            per_count += 1

    if pe_values:
        frappe.db.sql("""
            INSERT INTO "tabPayment Entry" (
                name, owner, creation, modified, modified_by,
                docstatus, posting_date, company,
                payment_type, party_type, party, party_name,
                paid_from, paid_to,
                paid_amount, base_paid_amount, received_amount, base_received_amount,
                target_exchange_rate, paid_from_account_currency, paid_to_account_currency,
                remarks,
                status, legacy_payment_id
            ) VALUES {}
        """.format(",".join(pe_values)))

    if per_values:
        frappe.db.sql("""
            INSERT INTO "tabPayment Entry Reference" (
                name, owner, creation, modified, modified_by,
                parent, parenttype, parentfield, idx,
                reference_doctype, reference_name,
                allocated_amount,
                exchange_rate, docstatus
            ) VALUES {}
        """.format(",".join(per_values)))

    frappe.db.commit()
    if (idx + CHUNK) % 20000 == 0 or idx + CHUNK >= len(payments):
        print("  Inserted {}/{} payments...".format(min(idx + CHUNK, len(payments)), len(payments)))

print("  {} Payment Entries, {} references [{:.0f}s]".format(pe_count, per_count, time.time() - t0))

# ═══════════════════════════════════════════════════════════════
# PHASE 6: GL ENTRIES — Invoices (receivable + income + TPS + TVQ)
# ═══════════════════════════════════════════════════════════════
print("\n" + "="*60)
print("PHASE 6: GL ENTRIES — INVOICES")
print("="*60)
t0 = time.time()

# Receivable side (debit for positive invoices, credit for returns)
frappe.db.sql("""
    INSERT INTO "tabGL Entry" (
        name, owner, creation, modified, modified_by, docstatus,
        posting_date, company, fiscal_year,
        account, party_type, party, account_currency,
        debit_in_account_currency, debit, credit_in_account_currency, credit,
        voucher_type, voucher_no, against,
        is_opening, is_cancelled
    )
    SELECT
        'gir-' || si.name, 'Administrator', NOW(), NOW(), 'Administrator', 1,
        si.posting_date, 'TARGO',
        CASE
            WHEN EXTRACT(MONTH FROM si.posting_date) >= 7
            THEN EXTRACT(YEAR FROM si.posting_date)::text || '-' || (EXTRACT(YEAR FROM si.posting_date) + 1)::text
            ELSE (EXTRACT(YEAR FROM si.posting_date) - 1)::text || '-' || EXTRACT(YEAR FROM si.posting_date)::text
        END,
        'Comptes clients - T', 'Customer', si.customer, 'CAD',
        GREATEST(si.grand_total, 0), GREATEST(si.grand_total, 0),
        GREATEST(-si.grand_total, 0), GREATEST(-si.grand_total, 0),
        'Sales Invoice', si.name, 'Autres produits d''exploitation - T',
        'No', 0
    FROM "tabSales Invoice" si
    WHERE si.docstatus = 1
""")
frappe.db.commit()
gl_recv = frappe.db.sql("SELECT COUNT(*) FROM \"tabGL Entry\" WHERE name LIKE 'gir-%%'")[0][0]
print("  Receivable GL entries: {}".format(gl_recv))

# Income side — one GL entry per income_account per invoice (items may use different GL accounts)
frappe.db.sql("""
    INSERT INTO "tabGL Entry" (
        name, owner, creation, modified, modified_by, docstatus,
        posting_date, company, fiscal_year,
        account, account_currency,
        debit_in_account_currency, debit, credit_in_account_currency, credit,
        voucher_type, voucher_no, against,
        is_opening, is_cancelled
    )
    SELECT
        'gii-' || sii.parent || '-' || ROW_NUMBER() OVER (PARTITION BY sii.parent ORDER BY sii.income_account),
        'Administrator', NOW(), NOW(), 'Administrator', 1,
        si.posting_date, 'TARGO',
        CASE
            WHEN EXTRACT(MONTH FROM si.posting_date) >= 7
            THEN EXTRACT(YEAR FROM si.posting_date)::text || '-' || (EXTRACT(YEAR FROM si.posting_date) + 1)::text
            ELSE (EXTRACT(YEAR FROM si.posting_date) - 1)::text || '-' || EXTRACT(YEAR FROM si.posting_date)::text
        END,
        sii.income_account, 'CAD',
        GREATEST(-SUM(sii.net_amount), 0), GREATEST(-SUM(sii.net_amount), 0),
        GREATEST(SUM(sii.net_amount), 0), GREATEST(SUM(sii.net_amount), 0),
        'Sales Invoice', si.name, 'Comptes clients - T',
        'No', 0
    FROM "tabSales Invoice Item" sii
    JOIN "tabSales Invoice" si ON si.name = sii.parent
    WHERE si.docstatus = 1
    GROUP BY sii.parent, sii.income_account, si.posting_date
""")
frappe.db.commit()
gl_inc = frappe.db.sql("SELECT COUNT(*) FROM \"tabGL Entry\" WHERE name LIKE 'gii-%%'")[0][0]
print("  Income GL entries: {}".format(gl_inc))

# TPS GL entries
frappe.db.sql("""
    INSERT INTO "tabGL Entry" (
        name, owner, creation, modified, modified_by, docstatus,
        posting_date, company, fiscal_year,
        account, account_currency,
        debit_in_account_currency, debit, credit_in_account_currency, credit,
        voucher_type, voucher_no, against,
        is_opening, is_cancelled
    )
    SELECT
        'glt-' || stc.parent, 'Administrator', NOW(), NOW(), 'Administrator', 1,
        si.posting_date, 'TARGO',
        CASE
            WHEN EXTRACT(MONTH FROM si.posting_date) >= 7
            THEN EXTRACT(YEAR FROM si.posting_date)::text || '-' || (EXTRACT(YEAR FROM si.posting_date) + 1)::text
            ELSE (EXTRACT(YEAR FROM si.posting_date) - 1)::text || '-' || EXTRACT(YEAR FROM si.posting_date)::text
        END,
        '2300 - TPS perçue - T', 'CAD',
        GREATEST(-stc.tax_amount, 0), GREATEST(-stc.tax_amount, 0),
        GREATEST(stc.tax_amount, 0), GREATEST(stc.tax_amount, 0),
        'Sales Invoice', si.name, 'Comptes clients - T',
        'No', 0
    FROM "tabSales Taxes and Charges" stc
    JOIN "tabSales Invoice" si ON si.name = stc.parent
    WHERE si.docstatus = 1 AND stc.account_head = '2300 - TPS perçue - T'
""")
frappe.db.commit()
gl_tps = frappe.db.sql("SELECT COUNT(*) FROM \"tabGL Entry\" WHERE name LIKE 'glt-%%'")[0][0]
print("  TPS GL entries: {}".format(gl_tps))

# TVQ GL entries
frappe.db.sql("""
    INSERT INTO "tabGL Entry" (
        name, owner, creation, modified, modified_by, docstatus,
        posting_date, company, fiscal_year,
        account, account_currency,
        debit_in_account_currency, debit, credit_in_account_currency, credit,
        voucher_type, voucher_no, against,
        is_opening, is_cancelled
    )
    SELECT
        'glq-' || stc.parent, 'Administrator', NOW(), NOW(), 'Administrator', 1,
        si.posting_date, 'TARGO',
        CASE
            WHEN EXTRACT(MONTH FROM si.posting_date) >= 7
            THEN EXTRACT(YEAR FROM si.posting_date)::text || '-' || (EXTRACT(YEAR FROM si.posting_date) + 1)::text
            ELSE (EXTRACT(YEAR FROM si.posting_date) - 1)::text || '-' || EXTRACT(YEAR FROM si.posting_date)::text
        END,
        '2350 - TVQ perçue - T', 'CAD',
        GREATEST(-stc.tax_amount, 0), GREATEST(-stc.tax_amount, 0),
        GREATEST(stc.tax_amount, 0), GREATEST(stc.tax_amount, 0),
        'Sales Invoice', si.name, 'Comptes clients - T',
        'No', 0
    FROM "tabSales Taxes and Charges" stc
    JOIN "tabSales Invoice" si ON si.name = stc.parent
    WHERE si.docstatus = 1 AND stc.account_head = '2350 - TVQ perçue - T'
""")
frappe.db.commit()
gl_tvq = frappe.db.sql("SELECT COUNT(*) FROM \"tabGL Entry\" WHERE name LIKE 'glq-%%'")[0][0]
print("  TVQ GL entries: {}".format(gl_tvq))

print("  Invoice GL total: {} [{:.0f}s]".format(gl_recv + gl_inc + gl_tps + gl_tvq, time.time() - t0))

# ═══════════════════════════════════════════════════════════════
# PHASE 7: GL ENTRIES — Payments (bank + receivable)
# ═══════════════════════════════════════════════════════════════
print("\n" + "="*60)
print("PHASE 7: GL ENTRIES — PAYMENTS")
print("="*60)
t0 = time.time()

# Bank side (debit bank)
frappe.db.sql("""
    INSERT INTO "tabGL Entry" (
        name, owner, creation, modified, modified_by, docstatus,
        posting_date, company, fiscal_year,
        account, account_currency,
        debit_in_account_currency, debit, credit_in_account_currency, credit,
        voucher_type, voucher_no, against,
        is_opening, is_cancelled
    )
    SELECT
        'gpb-' || pe.name, 'Administrator', NOW(), NOW(), 'Administrator', 1,
        pe.posting_date, 'TARGO',
        CASE
            WHEN EXTRACT(MONTH FROM pe.posting_date) >= 7
            THEN EXTRACT(YEAR FROM pe.posting_date)::text || '-' || (EXTRACT(YEAR FROM pe.posting_date) + 1)::text
            ELSE (EXTRACT(YEAR FROM pe.posting_date) - 1)::text || '-' || EXTRACT(YEAR FROM pe.posting_date)::text
        END,
        'Banque - T', 'CAD',
        pe.paid_amount, pe.paid_amount, 0, 0,
        'Payment Entry', pe.name, 'Comptes clients - T',
        'No', 0
    FROM "tabPayment Entry" pe
    WHERE pe.docstatus = 1
""")
frappe.db.commit()

# Receivable side (credit receivable)
frappe.db.sql("""
    INSERT INTO "tabGL Entry" (
        name, owner, creation, modified, modified_by, docstatus,
        posting_date, company, fiscal_year,
        account, party_type, party, account_currency,
        debit_in_account_currency, debit, credit_in_account_currency, credit,
        voucher_type, voucher_no, against,
        is_opening, is_cancelled
    )
    SELECT
        'gpr-' || pe.name, 'Administrator', NOW(), NOW(), 'Administrator', 1,
        pe.posting_date, 'TARGO',
        CASE
            WHEN EXTRACT(MONTH FROM pe.posting_date) >= 7
            THEN EXTRACT(YEAR FROM pe.posting_date)::text || '-' || (EXTRACT(YEAR FROM pe.posting_date) + 1)::text
            ELSE (EXTRACT(YEAR FROM pe.posting_date) - 1)::text || '-' || EXTRACT(YEAR FROM pe.posting_date)::text
        END,
        'Comptes clients - T', 'Customer', pe.party, 'CAD',
        0, 0, pe.paid_amount, pe.paid_amount,
        'Payment Entry', pe.name, 'Banque - T',
        'No', 0
    FROM "tabPayment Entry" pe
    WHERE pe.docstatus = 1
""")
frappe.db.commit()

gl_pmt = frappe.db.sql("SELECT COUNT(*) FROM \"tabGL Entry\" WHERE name LIKE 'gpb-%%' OR name LIKE 'gpr-%%'")[0][0]
print("  Payment GL entries: {} [{:.0f}s]".format(gl_pmt, time.time() - t0))

# ═══════════════════════════════════════════════════════════════
# PHASE 8: PAYMENT LEDGER ENTRIES
# ═══════════════════════════════════════════════════════════════
print("\n" + "="*60)
print("PHASE 8: PAYMENT LEDGER ENTRIES")
print("="*60)
t0 = time.time()

# PLE for invoices (self-referencing: against_voucher = self)
frappe.db.sql("""
    INSERT INTO "tabPayment Ledger Entry" (
        name, owner, creation, modified, modified_by, docstatus,
        posting_date, company,
        account_type, account, account_currency,
        party_type, party,
        due_date,
        voucher_type, voucher_no,
        against_voucher_type, against_voucher_no,
        amount, amount_in_account_currency,
        delinked, remarks
    )
    SELECT
        'ple-' || si.name, 'Administrator', NOW(), NOW(), 'Administrator', 1,
        si.posting_date, 'TARGO',
        'Receivable', 'Comptes clients - T', 'CAD',
        'Customer', si.customer,
        COALESCE(si.due_date, si.posting_date),
        'Sales Invoice', si.name,
        'Sales Invoice', si.name,
        si.grand_total, si.grand_total,
        0, 'Invoice posting'
    FROM "tabSales Invoice" si
    WHERE si.docstatus = 1
""")
frappe.db.commit()
ple_inv = frappe.db.sql("SELECT COUNT(*) FROM \"tabPayment Ledger Entry\" WHERE name LIKE 'ple-SINV-%%'")[0][0]
print("  Invoice PLE: {}".format(ple_inv))

# PLE for allocated payment references
frappe.db.sql("""
    INSERT INTO "tabPayment Ledger Entry" (
        name, owner, creation, modified, modified_by, docstatus,
        posting_date, company,
        account_type, account, account_currency,
        party_type, party,
        due_date,
        voucher_type, voucher_no,
        against_voucher_type, against_voucher_no,
        amount, amount_in_account_currency,
        delinked, remarks
    )
    SELECT
        'ple-' || per.name, 'Administrator', NOW(), NOW(), 'Administrator', 1,
        pe.posting_date, 'TARGO',
        'Receivable', 'Comptes clients - T', 'CAD',
        'Customer', pe.party,
        pe.posting_date,
        'Payment Entry', pe.name,
        'Sales Invoice', per.reference_name,
        -per.allocated_amount, -per.allocated_amount,
        0, 'Payment allocation'
    FROM "tabPayment Entry Reference" per
    JOIN "tabPayment Entry" pe ON pe.name = per.parent
    WHERE pe.docstatus = 1 AND per.reference_doctype = 'Sales Invoice'
""")
frappe.db.commit()
ple_per = frappe.db.sql("SELECT COUNT(*) FROM \"tabPayment Ledger Entry\" WHERE name LIKE 'ple-PER-%%'")[0][0]
print("  Payment allocation PLE: {}".format(ple_per))

# PLE for unallocated payments (no references)
frappe.db.sql("""
    INSERT INTO "tabPayment Ledger Entry" (
        name, owner, creation, modified, modified_by, docstatus,
        posting_date, company,
        account_type, account, account_currency,
        party_type, party,
        due_date,
        voucher_type, voucher_no,
        against_voucher_type, against_voucher_no,
        amount, amount_in_account_currency,
        delinked, remarks
    )
    SELECT
        'ple-' || pe.name, 'Administrator', NOW(), NOW(), 'Administrator', 1,
        pe.posting_date, 'TARGO',
        'Receivable', 'Comptes clients - T', 'CAD',
        'Customer', pe.party,
        pe.posting_date,
        'Payment Entry', pe.name,
        'Payment Entry', pe.name,
        -pe.paid_amount, -pe.paid_amount,
        0, 'Unallocated payment'
    FROM "tabPayment Entry" pe
    WHERE pe.docstatus = 1
      AND NOT EXISTS (
          SELECT 1 FROM "tabPayment Entry Reference" per
          WHERE per.parent = pe.name
      )
""")
frappe.db.commit()
ple_unalloc = frappe.db.sql("SELECT COUNT(*) FROM \"tabPayment Ledger Entry\" WHERE remarks = 'Unallocated payment'")[0][0]
print("  Unallocated payment PLE: {}".format(ple_unalloc))

# PLE for credit allocations (credit note → target invoice)
print("\n  Creating credit allocation PLE...")
credit_ple_count = 0
for alloc in credit_allocs:
    source_sinv = inv_id_to_sinv.get(alloc["source_invoice_id"])
    target_sinv = inv_id_to_sinv.get(alloc["target_invoice_id"])
    if source_sinv and target_sinv:
        credit_ple_count += 1

# Use temp table for batch insert
frappe.db.sql("DROP TABLE IF EXISTS _tmp_credit_alloc")
frappe.db.commit()
frappe.db.sql("""
    CREATE TABLE _tmp_credit_alloc (
        id SERIAL PRIMARY KEY,
        source_sinv VARCHAR(140),
        target_sinv VARCHAR(140),
        amount DOUBLE PRECISION
    )
""")
frappe.db.commit()

resolved_credits = []
for alloc in credit_allocs:
    source_sinv = inv_id_to_sinv.get(alloc["source_invoice_id"])
    target_sinv = inv_id_to_sinv.get(alloc["target_invoice_id"])
    if source_sinv and target_sinv:
        resolved_credits.append((source_sinv, target_sinv, alloc["amount"]))

for idx in range(0, len(resolved_credits), 5000):
    batch = resolved_credits[idx:idx+5000]
    values = ",".join(["('{}', '{}', {})".format(s, t, a) for s, t, a in batch])
    frappe.db.sql("INSERT INTO _tmp_credit_alloc (source_sinv, target_sinv, amount) VALUES {}".format(values))
    frappe.db.commit()

# Delete self-referencing PLE for credit invoices that have allocations
frappe.db.sql("""
    DELETE FROM "tabPayment Ledger Entry"
    WHERE name IN (
        SELECT 'ple-' || ca.source_sinv
        FROM _tmp_credit_alloc ca
    )
""")
frappe.db.commit()

# Insert credit allocation PLE
frappe.db.sql("""
    INSERT INTO "tabPayment Ledger Entry" (
        name, owner, creation, modified, modified_by, docstatus,
        posting_date, company,
        account_type, account, account_currency,
        party_type, party,
        due_date,
        voucher_type, voucher_no,
        against_voucher_type, against_voucher_no,
        amount, amount_in_account_currency,
        delinked, remarks
    )
    SELECT
        'plc-' || ca.id, 'Administrator', NOW(), NOW(), 'Administrator', 1,
        si.posting_date, 'TARGO',
        'Receivable', 'Comptes clients - T', 'CAD',
        'Customer', si.customer,
        COALESCE(si.due_date, si.posting_date),
        'Sales Invoice', ca.source_sinv,
        'Sales Invoice', ca.target_sinv,
        -ca.amount, -ca.amount,
        0, 'Credit allocation from return invoice'
    FROM _tmp_credit_alloc ca
    JOIN "tabSales Invoice" si ON si.name = ca.source_sinv
""")
frappe.db.commit()
ple_credit = frappe.db.sql("SELECT COUNT(*) FROM \"tabPayment Ledger Entry\" WHERE name LIKE 'plc-%%'")[0][0]
print("  Credit allocation PLE: {}".format(ple_credit))

frappe.db.sql("DROP TABLE IF EXISTS _tmp_credit_alloc")
frappe.db.commit()

# PLE for reversal invoices (matched via notes OR reversement memos, NOT credit payments)
all_reversal_matches = reversal_matches + reversement_matches
if all_reversal_matches:
    frappe.db.sql("DROP TABLE IF EXISTS _tmp_reversal_match")
    frappe.db.commit()
    frappe.db.sql("""
        CREATE TABLE _tmp_reversal_match (
            id SERIAL PRIMARY KEY,
            credit_sinv VARCHAR(140),
            target_sinv VARCHAR(140)
        )
    """)
    frappe.db.commit()

    for idx_r in range(0, len(all_reversal_matches), 5000):
        batch = all_reversal_matches[idx_r:idx_r+5000]
        values = ",".join(["('{}', '{}')".format(c, t) for c, t in batch])
        frappe.db.sql("INSERT INTO _tmp_reversal_match (credit_sinv, target_sinv) VALUES {}".format(values))
        frappe.db.commit()

    # Delete self-referencing PLE for reversal credit invoices (only actual returns)
    frappe.db.sql("""
        DELETE FROM "tabPayment Ledger Entry"
        WHERE name IN (
            SELECT 'ple-' || rm.credit_sinv FROM _tmp_reversal_match rm
            JOIN "tabSales Invoice" si ON si.name = rm.credit_sinv
            WHERE si.is_return = 1
        )
    """)
    frappe.db.commit()

    # Insert reversal PLE (credit invoice → target invoice)
    frappe.db.sql("""
        INSERT INTO "tabPayment Ledger Entry" (
            name, owner, creation, modified, modified_by, docstatus,
            posting_date, company,
            account_type, account, account_currency,
            party_type, party,
            due_date,
            voucher_type, voucher_no,
            against_voucher_type, against_voucher_no,
            amount, amount_in_account_currency,
            delinked, remarks
        )
        SELECT
            'plr-' || rm.id, 'Administrator', NOW(), NOW(), 'Administrator', 1,
            si.posting_date, 'TARGO',
            'Receivable', 'Comptes clients - T', 'CAD',
            'Customer', si.customer,
            COALESCE(si.due_date, si.posting_date),
            'Sales Invoice', rm.credit_sinv,
            'Sales Invoice', rm.target_sinv,
            si.grand_total, si.grand_total,
            0, 'Reversal allocation (from notes)'
        FROM _tmp_reversal_match rm
        JOIN "tabSales Invoice" si ON si.name = rm.credit_sinv
        WHERE si.is_return = 1
    """)
    frappe.db.commit()

    frappe.db.sql("DROP TABLE IF EXISTS _tmp_reversal_match")
    frappe.db.commit()

ple_reversal = frappe.db.sql("SELECT COUNT(*) FROM \"tabPayment Ledger Entry\" WHERE name LIKE 'plr-%%'")[0][0]
print("  Reversal PLE: {}".format(ple_reversal))

print("  Total PLE: {} [{:.0f}s]".format(
    ple_inv + ple_per + ple_unalloc + ple_credit + ple_reversal,
    time.time() - t0
))

# ═══════════════════════════════════════════════════════════════
# PHASE 9: OUTSTANDING + STATUS
# ═══════════════════════════════════════════════════════════════
print("\n" + "="*60)
print("PHASE 9: OUTSTANDING + STATUS")
print("="*60)
t0 = time.time()

# Outstanding = SUM of all PLE against this invoice
frappe.db.sql("""
    UPDATE "tabSales Invoice" si
    SET outstanding_amount = COALESCE((
        SELECT SUM(ple.amount)
        FROM "tabPayment Ledger Entry" ple
        WHERE ple.against_voucher_type = 'Sales Invoice'
          AND ple.against_voucher_no = si.name
          AND ple.delinked = 0
    ), si.grand_total)
    WHERE si.docstatus = 1
""")
frappe.db.commit()
print("  Outstanding amounts calculated")

# All return invoices should have outstanding = 0 (settled by definition)
frappe.db.sql("""
    UPDATE "tabSales Invoice"
    SET outstanding_amount = 0
    WHERE is_return = 1 AND docstatus = 1
""")
frappe.db.commit()

# Fix remaining overpaid non-return invoices (legacy double-settlement: paid + reversed)
# These were settled in legacy — mark as paid with a correction note
overpaid_fix = frappe.db.sql("""
    SELECT name, outstanding_amount FROM "tabSales Invoice"
    WHERE docstatus = 1 AND is_return != 1 AND outstanding_amount < -0.005
""", as_dict=True)
if overpaid_fix:
    overpaid_names = [r['name'] for r in overpaid_fix]
    overpaid_total = sum(float(r['outstanding_amount']) for r in overpaid_fix)
    # Delink the extra reversal PLE entries that caused overpayment
    frappe.db.sql("""
        UPDATE "tabPayment Ledger Entry"
        SET delinked = 1, remarks = 'Delinked: legacy double-settlement (paid + reversed)'
        WHERE name LIKE 'plr-%%'
          AND against_voucher_no IN ({})
    """.format(",".join(["'{}'".format(n) for n in overpaid_names])))
    frappe.db.commit()
    # Recalculate outstanding on these invoices
    frappe.db.sql("""
        UPDATE "tabSales Invoice" si
        SET outstanding_amount = COALESCE((
            SELECT SUM(ple.amount)
            FROM "tabPayment Ledger Entry" ple
            WHERE ple.against_voucher_type = 'Sales Invoice'
              AND ple.against_voucher_no = si.name
              AND ple.delinked = 0
        ), si.grand_total)
        WHERE si.name IN ({})
    """.format(",".join(["'{}'".format(n) for n in overpaid_names])))
    frappe.db.commit()
    # Any still negative (e.g. rounding, double credit alloc) — force to 0
    frappe.db.sql("""
        UPDATE "tabSales Invoice"
        SET outstanding_amount = 0
        WHERE docstatus = 1 AND is_return != 1 AND outstanding_amount < -0.005
    """)
    frappe.db.commit()
    print("  Fixed {} overpaid invoices (${:.2f} total) — legacy double-settlement".format(
        len(overpaid_fix), overpaid_total))

# Statuses
frappe.db.sql("""UPDATE "tabSales Invoice" SET status = 'Return' WHERE docstatus = 1 AND is_return = 1""")
frappe.db.sql("""UPDATE "tabSales Invoice" SET status = 'Paid' WHERE docstatus = 1 AND is_return != 1 AND ROUND(outstanding_amount, 2) = 0""")
frappe.db.sql("""UPDATE "tabSales Invoice" SET status = 'Overdue' WHERE docstatus = 1 AND is_return != 1 AND outstanding_amount > 0.005 AND COALESCE(due_date, posting_date) < CURRENT_DATE""")
frappe.db.sql("""UPDATE "tabSales Invoice" SET status = 'Unpaid' WHERE docstatus = 1 AND is_return != 1 AND outstanding_amount > 0.005 AND COALESCE(due_date, posting_date) >= CURRENT_DATE""")
frappe.db.sql("""UPDATE "tabSales Invoice" SET status = 'Credit Note Issued' WHERE docstatus = 1 AND is_return != 1 AND outstanding_amount < -0.005""")
frappe.db.commit()
print("  Statuses updated [{:.0f}s]".format(time.time() - t0))

# ═══════════════════════════════════════════════════════════════
# VERIFICATION
# ═══════════════════════════════════════════════════════════════
print("\n" + "="*60)
print("VERIFICATION")
print("="*60)

# Counts
si_count = frappe.db.sql('SELECT COUNT(*) FROM "tabSales Invoice" WHERE docstatus = 1')[0][0]
pe_final = frappe.db.sql('SELECT COUNT(*) FROM "tabPayment Entry" WHERE docstatus = 1')[0][0]
gl_count = frappe.db.sql('SELECT COUNT(*) FROM "tabGL Entry"')[0][0]
ple_count = frappe.db.sql('SELECT COUNT(*) FROM "tabPayment Ledger Entry"')[0][0]
print("  Sales Invoices: {}".format(si_count))
print("  Payment Entries: {}".format(pe_final))
print("  GL Entries: {}".format(gl_count))
print("  PLE Entries: {}".format(ple_count))

# GL Balance
gl_balance = frappe.db.sql("""
    SELECT
        ROUND(SUM(debit), 2) as total_debit,
        ROUND(SUM(credit), 2) as total_credit,
        ROUND(SUM(debit) - SUM(credit), 2) as diff
    FROM "tabGL Entry"
""", as_dict=True)[0]
print("\n  GL Balance: debit=${} credit=${} diff=${}".format(
    gl_balance['total_debit'], gl_balance['total_credit'], gl_balance['diff']))

# Status breakdown
statuses = frappe.db.sql("""
    SELECT status, COUNT(*) as cnt
    FROM "tabSales Invoice" WHERE docstatus = 1
    GROUP BY status ORDER BY cnt DESC
""", as_dict=True)
print("\n  Invoice status breakdown:")
for s in statuses:
    print("    {}: {}".format(s['status'], s['cnt']))

# Outstanding totals
outstanding = frappe.db.sql("""
    SELECT
        ROUND(SUM(CASE WHEN outstanding_amount > 0.005 THEN outstanding_amount ELSE 0 END), 2) as owed,
        ROUND(SUM(CASE WHEN outstanding_amount < -0.005 THEN outstanding_amount ELSE 0 END), 2) as overpaid
    FROM "tabSales Invoice" WHERE docstatus = 1
""", as_dict=True)[0]
print("\n  Outstanding: ${} owed | ${} overpaid".format(outstanding['owed'], outstanding['overpaid']))

# Check invoice 638567
inv_check = frappe.db.sql("""
    SELECT name, grand_total, outstanding_amount, status, customer
    FROM "tabSales Invoice" WHERE legacy_invoice_id = 638567
""", as_dict=True)
if inv_check:
    i = inv_check[0]
    print("\n  Invoice 638567: {} | total={} | outstanding={} | {} | {}".format(
        i['name'], i['grand_total'], i['outstanding_amount'], i['status'], i['customer']))
else:
    print("\n  Invoice 638567: NOT FOUND (customer may not be mapped)")

elapsed = time.time() - T_TOTAL
print("\n" + "="*60)
print("CLEAN REIMPORT COMPLETE in {:.0f}s".format(elapsed))
print("="*60)
