#!/usr/bin/env python3
import re, json, csv
from collections import defaultdict
from pathlib import Path

BASE = Path('/home/sebas/work/gastos-europa')
SRC = BASE / 'raw' / 'extracted-text.txt'
VSRC = BASE / 'raw' / 'vicky-manual.txt'
OUT = BASE / 'derived'
DATA = BASE / 'data'
EXCLUDED = DATA / 'excluded.json'

KNOWN = [
    ('albert heijn', 'Supermercado neerlandés.', 'alta'),
    ('jumbo', 'Cadena de supermercado en Países Bajos.', 'alta'),
    ('condomerie', 'Sex shop conocido de Ámsterdam.', 'alta'),
    ('primark', 'Tienda de ropa y básicos de bajo costo.', 'alta'),
    ('friet point', 'Local de papas fritas y comida rápida.', 'alta'),
    ('ns groep', 'Ferrocarriles neerlandeses / transporte NS.', 'alta'),
    ('ns-amsterdam', 'Transporte ferroviario neerlandés / NS.', 'alta'),
    ('schiphol', 'Compra o transporte en el aeropuerto Schiphol.', 'alta'),
    ('r.j. mulder', 'Tienda de quesos y delicatessen Henri Willig / R.J. Mulder en Ámsterdam.', 'media'),
    ('het karbeel', 'Restaurante o brasserie en Warmoesstraat, Ámsterdam.', 'alta'),
    ('la belle super', 'Minimercado o grocery shop en Ámsterdam.', 'alta'),
    ('temple bar', 'Pub irlandés / bar en Ámsterdam.', 'alta'),
    ('mcdonald', 'Comida rápida McDonald’s.', 'alta'),
    ('victoria hotel', 'Consumo en hotel.', 'alta'),
    ('flagship amsterdam', 'Paseo turístico en barco por canales de Ámsterdam.', 'alta'),
    ('il primo', 'Restaurante italiano.', 'media'),
    ('van gogh museum', 'Entrada al Museo Van Gogh.', 'alta'),
    ('anne frank', 'Entrada a la Casa de Ana Frank.', 'alta'),
    ('twinkies bussum', 'Panadería, café o local de brunch en Bussum.', 'alta'),
    ('lamor amsterdam', 'Bar o restaurante La Mor / Lamor en Ámsterdam.', 'media'),
    ('the bulldog', 'Coffee shop/bar de Ámsterdam.', 'alta'),
    ('game galaxy', 'Arcade o local de videojuegos en Ámsterdam.', 'alta'),
    ('cafe katoen', 'Café o restaurante Café Katoen en Ámsterdam.', 'alta'),
    ('b.smit', 'Tienda de souvenirs, regalos o retail pequeño en Ámsterdam.', 'media'),
    ('ghm b b b.v.', 'Bar, café o comercio gastronómico local en Ámsterdam.', 'media'),
    ('city phone internet', 'Tienda de telefonía o accesorios.', 'alta'),
    ('adolfo suarez', 'Cargo en aeropuerto Madrid-Barajas.', 'alta'),
    ('pending.uber', 'Viaje o cargo pendiente de Uber.', 'alta'),
    ('intruso', 'Bar, café o restaurante Intruso en Madrid.', 'alta'),
    ('el corte ingles', 'Gran tienda por departamentos.', 'alta'),
    ('lefties', 'Tienda de ropa del grupo Inditex.', 'alta'),
    ('phone shop', 'Tienda de telefonía o accesorios.', 'alta'),
    ('uniqlo', 'Tienda de ropa Uniqlo.', 'alta'),
    ('cabify', 'Viaje en Cabify.', 'alta'),
    ('amazon web services', 'Cargo de AWS / infraestructura cloud.', 'alta'),
    ('vercel domains', 'Dominio o servicio de Vercel.', 'alta'),
    ('village rosa', 'Alojamiento en Brasil; fuera del viaje Europa.', 'media'),
    ('openai', 'Suscripción de OpenAI / ChatGPT.', 'alta'),
    ('claude.ai', 'Suscripción de Claude / Anthropic.', 'alta'),
    ('omio berlin', 'Pasaje o reserva de transporte vía Omio.', 'alta'),
    ('goeuro corp', 'Pasaje o reserva de transporte de Omio/GoEuro.', 'alta'),
    ('aerobus', 'Traslado en bus al/del aeropuerto.', 'alta'),
    ('louvreticket', 'Entrada al Museo del Louvre.', 'alta'),
    ('sagrada familia', 'Entrada a la Sagrada Familia.', 'alta'),
    ('buckaroo', 'Pago procesado por Buckaroo; corresponde a entradas de Anne Frank.', 'alta'),
    ('versailles', 'Entrada o reserva vinculada a Versalles.', 'alta'),
    ('biglietteriamusei.vatic', 'Entrada a Museos Vaticanos.', 'alta'),
    ('duomo.firenze', 'Entrada al Duomo de Florencia.', 'alta'),
    ('museo del prado', 'Entrada al Museo del Prado.', 'alta'),
    ('de kroon van singel', 'Bar o restaurante De Kroon van Singel, Ámsterdam.', 'alta'),
    ('molenmuseum de zaansche', 'Entrada o visita al museo de Zaansche Molen.', 'alta'),
    ('old bridge', 'Souvenir shop o retail local Old Bridge en Países Bajos.', 'alta'),
    ('sam tabak', 'Tienda de tabaco y souvenirs en Ámsterdam.', 'alta'),
    ('la pampa', 'Restaurante parrilla.', 'alta'),
    ('c&m*singel', 'Comercio pequeño sobre Singel 506, Ámsterdam; rubro exacto no confirmado.', 'baja'),
    ('croissanterie outmayer', 'Panadería o cafetería Croissanterie Outmayer.', 'alta'),
    ('papizza', 'Local de pizza rápida.', 'alta'),
    ('ta-wha chin arts', 'Tienda de artesanías, regalos o souvenirs.', 'alta'),
    ('hotel beursstraat', 'Alojamiento / hotel en Ámsterdam.', 'alta'),
    ('kamyin', 'Supermercado o tienda de alimentos asiáticos Kam Yin.', 'alta'),
    ('zara madrid', 'Tienda de ropa Zara.', 'alta'),
    ('butragueno esteban', 'Tienda o comercio local de retail en Madrid.', 'media'),
    ('vivaz rosaleda', 'Bar o restaurante Vivaz en Madrid.', 'alta'),
    ('viandas de salamanca', 'Tienda gourmet / fiambres / alimentos.', 'alta'),
    ('dekorazoncarmen', 'Tienda de decoración o regalos en Madrid.', 'alta'),
    ('ch lumiere', 'Alojamiento / guesthouse en Madrid al inicio del viaje.', 'alta'),
    ('taberna la daniela', 'Taberna o restaurante madrileño.', 'alta'),
    ('pharmacie des', 'Farmacia o perfumería en Francia.', 'alta'),
    ('carrefour city', 'Supermercado Carrefour City.', 'alta'),
    ('franprix', 'Supermercado Franprix.', 'alta'),
    ('lindt spruengli', 'Chocolatería o tienda Lindt.', 'alta'),
    ('bouillon pigalle', 'Restaurante Bouillon Pigalle en París.', 'alta'),
    ('tonnarello', 'Restaurante Tonnarello en Roma.', 'alta'),
    ('frigidarium', 'Heladería o postres en Roma.', 'alta'),
    ('filippo calabria', 'Local gastronómico o comercio de alimentos en Italia.', 'media'),
    ('bistrot coronari', 'Bistró o restaurante en Roma.', 'alta'),
    ('pizzeria alle carrette', 'Pizzería en Roma.', 'alta'),
    ('pizzicaroli', 'Restaurante o comida rápida italiana I Pizzicaroli.', 'alta'),
    ('il chiosco', 'Kiosco o local de comida.', 'alta'),
    ('eis srl', 'Heladería o local de postres / gelato.', 'alta'),
    ('trapizzino', 'Local de comida italiana Trapizzino.', 'alta'),
    ("all'antico vinaio", 'Sandwichería / local gastronómico All’Antico Vinaio.', 'alta'),
    ('neu*ditech', 'Tienda de electrónica o accesorios.', 'media'),
    ('romacentro', 'Comercio o servicio turístico en Roma centro.', 'media'),
    ('dms pantheon self', 'Comercio de autoservicio cerca del Panteón.', 'alta'),
    ('cremonini', 'Comida o servicio gastronómico Cremonini.', 'alta'),
    ('guelfi tuscany food', 'Tienda gastronómica / alimentos de Toscana.', 'alta'),
    ('rossopomodoro', 'Restaurante/pizzería Rossopomodoro.', 'alta'),
    ('panjetan', 'Comercio local en Firenze; rubro exacto no confirmado.', 'media'),
    ('conad', 'Supermercado Conad.', 'alta'),
    ('rooster firenze', 'Restaurante o comida rápida en Firenze.', 'alta'),
    ('ultima srls', 'Comercio local en Italia; rubro exacto no confirmado.', 'media'),
    ('ristorante al sole', 'Restaurante Al Sole.', 'alta'),
    ('buffet mestre', 'Buffet o cafetería en Mestre/Venezia.', 'alta'),
    ('sha jahan', 'Restaurante indio o local gastronómico.', 'alta'),
    ('despar', 'Supermercado Despar.', 'alta'),
    ('axa b2c da', 'Compra de seguro o servicio AXA.', 'media'),
    ('firenze por santa mari', 'Compra o acceso cerca de Santa Maria Novella en Firenze.', 'media'),
    ('deel balance', 'Recarga o movimiento interno de saldo Deel.', 'alta'),
    ('airbnb', 'Cobro, autorización o reverso vinculado a Airbnb.', 'media'),
    ('deel', 'Transferencia o pago recibido vía Deel.', 'alta'),
    ('interactive brok', 'Transferencia desde Interactive Brokers.', 'alta'),
    ('fee for international transaction', 'Comisión bancaria por compra internacional.', 'alta'),
    ('monthly account service fee', 'Cargo mensual de mantenimiento de cuenta.', 'alta'),
]

def guess_desc(merchant, category, is_fee, typ, country, trip_scope):
    low = merchant.lower()
    for key, desc, conf in KNOWN:
        if key in low:
            return desc, conf, 'known_or_web'
    if typ == 'income':
        return 'Ingreso, reintegro o transferencia a favor.', 'alta', 'heuristic'
    if is_fee:
        return 'Comisión o cargo bancario.', 'alta', 'heuristic'
    cat_map = {
        'transport': 'Gasto de transporte o traslado.',
        'attraction': 'Entrada o actividad turística.',
        'groceries': 'Compra de supermercado o alimentos.',
        'food_drink': 'Comida, bar o cafetería.',
        'shopping': 'Compra de ropa, accesorios o retail.',
        'lodging': 'Alojamiento u hotel.',
        'subscription': 'Suscripción o servicio digital.',
        'transfer': 'Transferencia o movimiento de dinero.',
        'other': 'Comercio local; rubro exacto no confirmado.',
        'fee': 'Comisión o cargo bancario.',
    }
    desc = cat_map.get(category, 'Comercio local; rubro exacto no confirmado.')
    if trip_scope == 'non_trip' and country in ('US/CA', 'BR'):
        desc += ' Fuera del viaje Europa.'
    conf = 'media' if category not in ('other',) else 'baja'
    return desc, conf, 'heuristic'


def parse_sources():
    entries=[]
    photo=None; date=None; section=None
    for line in SRC.read_text().splitlines():
        if line.startswith('## photo-'):
            photo=line.strip('# ').strip(); date=None; section=None; continue
        s=line.strip()
        if not s: continue
        if s in ('Pending Transactions','Posted Transactions'):
            section='pending' if 'Pending' in s else 'posted'; continue
        if re.match(r'^\d{2}/\d{2}/\d{4}$', s):
            date=s; continue
        if s.startswith('- ') and ' — ' in s[2:]:
            merchant, amount_s = s[2:].rsplit(' — ',1)
            m = re.search(r'[-+]?\$?([0-9][0-9,]*\.?[0-9]*)', amount_s)
            if not m:
                continue
            num = m.group(0).replace('$','').replace(',','')
            amt=float(num)
            entries.append({'source':'sebas_bank','source_ref':photo,'paid_by':'S','date':date or '','status':section or '','merchant':merchant,'amount_usd':amt})
    date=None
    for line in VSRC.read_text().splitlines():
        s=line.strip()
        if not s or s.startswith(('Source:','Payer:','Currency ')): continue
        if re.match(r'^\d{2}/\d{2}/\d{4}$', s):
            date=s; continue
        if s.startswith('- ') and ' — ' in s[2:]:
            merchant, amount_s = s[2:].rsplit(' — ',1)
            m = re.search(r'[-+]?\$?([0-9][0-9,]*\.?[0-9]*)', amount_s)
            if not m:
                continue
            num = m.group(0).replace('$','').replace(',','')
            amt=float(num)
            entries.append({'source':'vicky_manual','source_ref':'vicky-manual','paid_by':'V','date':date or '','status':'manual','merchant':merchant,'amount_usd':amt})
    return entries


DEEL_AIRBNB_PHOTOS = {'photo-1389.jpg', 'photo-1390.jpg', 'photo-1391.jpg'}


def tx_key(e):
    return '|'.join([
        e.get('source',''),
        e.get('source_ref',''),
        e.get('paid_by',''),
        e.get('date',''),
        e.get('status',''),
        e.get('merchant',''),
        f"{e.get('amount_usd', 0):.2f}",
    ])


def excluded_keys():
    try:
        data = json.loads(EXCLUDED.read_text())
        if isinstance(data, list):
            return {str(x) for x in data if str(x).strip()}
        if isinstance(data, dict):
            return {str(k) for k,v in data.items() if v}
    except Exception:
        pass
    return set()


def enrich(e, idx):
    merchant=e['merchant']; low=merchant.lower(); amt=e['amount_usd']
    if e['source_ref'] in DEEL_AIRBNB_PHOTOS and 'airbnb' in low and amt > 0:
        amt = -amt
    typ='zero' if amt == 0 else ('expense' if amt < 0 else 'income')
    fee=('fee for international transaction' in low or 'monthly account service fee' in low)
    cat='other'; country=''; trip='europe_trip'
    if fee: cat='fee'
    elif any(x in low for x in ['omio','goeuro','cabify','uber','ns ','ns-','schiphol','aerop.','airport','luchthaven','aerobus']): cat='transport'
    elif any(x in low for x in ['museum','anne frank','sagrada','duomo','vatic','louvreticket','versailles','prado','flagship amsterdam','molenmuseum']): cat='attraction'
    elif any(x in low for x in ['albert heijn','jumbo','super','croissanterie','kamyin','viandas','carrefour','franprix','despar','conad']): cat='groceries'
    elif any(x in low for x in ['mcdonald','cafe','bar','friet','restaurant','temple bar','bulldog','il primo','twinkies','lamor','karbeel','katoen','intruso','la pampa','papizza','taberna','de kroon','vivaz','pancake','bouillon','frigidarium','pizzicaroli','bistrot','pizzeria','chiosco','trapizzino','antico vinaio','cremonini','guelfi','rossopomodoro','rooster','sha jahan','ristorante al sole','buffet mestre','tonnarello']): cat='food_drink'
    elif any(x in low for x in ['uniqlo','lefties','primark','el corte ingles','condomerie','phone shop','city phone','fa. b.smit','zara','dekorazon','tabak','arts & cra','butragueno','sam tabak','old bridge','premium retail','quotday','quotiday','miss manon','lindt','ditech','romacentro','market s.r.l.','panjeri','panjetan','pharmacie des']): cat='shopping'
    elif any(x in low for x in ['hotel','ch lumiere','beursstraat','village rosa']): cat='lodging'
    elif any(x in low for x in ['openai','claude.ai','aws.amazon','vercel','spotify']): cat='subscription'; trip='non_trip'
    elif any(x in low for x in ['airbnb']): cat='lodging'; trip='europe_trip' if e['source_ref'] in DEEL_AIRBNB_PHOTOS else 'non_trip'
    elif any(x in low for x in ['deel balance','deel','interactive brok']): cat='transfer'; trip='non_trip'
    if any(x in merchant for x in [' AMSTERDAM ','Amsterdam','SCHIPHOL','BUSSUM','DAMRAK','Albert Heijn','JUMBO','BCK*NS','BCK*GHM','Buckaroo','Singel','Beursstraat','KamYin','PAPIZZA']): country='NL'
    if any(x in merchant for x in [' MADRID ','GranVia','PRADO','SAGRADA','EL CORTE','LEFTIES','Cabify','ADOLFO SUAREZ','ZARA MADRID','SALAMANCA','CARMEN','ROSALEDA','BUTRAGUENO']): country='ES'
    if any(x in merchant for x in ['PARIS','VERSAILLES','LOUVRE','LUMIERE']): country='FR'
    if 'BERLIN' in merchant: country='DE'
    if 'DUOMO' in merchant: country='IT'
    if 'Vatic' in merchant or 'Vat VA' in merchant: country='VA'
    if 'IMBITUBA BR' in merchant: country='BR'; trip='non_trip'
    if 'WA ON' in merchant or 'CA ON' in merchant or 'Fort Myers FL' in merchant or 'FL ON' in merchant:
        if cat in ('subscription','transfer'): country='US/CA'
    if cat == 'subscription': trip='non_trip'
    desc, conf, desc_source = guess_desc(merchant, cat, fee, typ, country, trip)
    if e['source_ref'] in DEEL_AIRBNB_PHOTOS and 'airbnb' in low:
        desc = 'Alojamiento de Airbnb del viaje Europa.'
        conf = 'alta'
        desc_source = 'user_confirmed'
    return {**e,'amount_usd':amt,'tx_key':tx_key({**e, 'amount_usd': amt}),'id':f"{e['source_ref']}:{idx}",'type':typ,'is_fee':fee,'category_guess':cat,'country_guess':country,'trip_scope':trip,'allocation':'','description':desc,'description_confidence':conf,'description_source':desc_source}


def build():
    OUT.mkdir(parents=True, exist_ok=True)
    excluded = excluded_keys()
    all_entries=[enrich(e,i+1) for i,e in enumerate(parse_sources())]
    entries=[e for e in all_entries if e.get('tx_key') not in excluded]
    (OUT/'transactions.json').write_text(json.dumps(entries, indent=2, ensure_ascii=False))
    with (OUT/'transactions.csv').open('w', newline='') as f:
        w=csv.DictWriter(f, fieldnames=list(entries[0].keys())); w.writeheader(); w.writerows(entries)
    sum_all=sum(e['amount_usd'] for e in entries if e['type']=='expense')
    sum_trip=sum(e['amount_usd'] for e in entries if e['type']=='expense' and e['trip_scope']=='europe_trip')
    sum_nontrip=sum(e['amount_usd'] for e in entries if e['type']=='expense' and e['trip_scope']=='non_trip')
    sum_fees=sum(e['amount_usd'] for e in entries if e['is_fee'])
    sum_income=sum(e['amount_usd'] for e in entries if e['type']=='income')
    by_cat=defaultdict(float); by_country=defaultdict(float); by_payer=defaultdict(float)
    for e in entries:
        if e['type']=='expense' and e['trip_scope']=='europe_trip':
            by_cat[e['category_guess']]+= -e['amount_usd']
            by_country[e['country_guess'] or 'unknown']+= -e['amount_usd']
            by_payer[e['paid_by']]+= -e['amount_usd']
    summary={'count':len(entries),'excluded_count':len(all_entries)-len(entries),'image_count':len({e['source_ref'] for e in entries if e['source']=='sebas_bank'}),'manual_vicky_count':len([e for e in entries if e['paid_by']=='V']),'expense_total_all':round(-sum_all,2),'expense_total_trip':round(-sum_trip,2),'expense_total_non_trip':round(-sum_nontrip,2),'fees_total':round(-sum_fees,2),'income_total':round(sum_income,2),'by_category_trip':dict(sorted(by_cat.items(), key=lambda kv: kv[1], reverse=True)),'by_country_trip':dict(sorted(by_country.items(), key=lambda kv: kv[1], reverse=True)),'by_payer_trip':dict(sorted(by_payer.items()))}
    (OUT/'summary.json').write_text(json.dumps(summary, indent=2, ensure_ascii=False))

if __name__ == '__main__':
    build()
