"""
BSS Skill: Expense Report Analyzer
Parses an expense list from PROMPT (email, pasted table, or natural language)
and outputs: total, breakdown by category, top items, flagged anomalies.
Reduces expense reports by 75-90%.
"""
import re
import statistics
from collections import defaultdict

text = PROMPT  # injected by BIBIVIDI sandbox wrapper

# ── Category keyword map ──────────────────────────────────────────────────────

CATEGORY_KEYWORDS = {
    "Travel": [
        "taxi", "uber", "train", "flight", "avion", "billet", "sncf", "tgv",
        "metro", "métro", "bus", "parking", "voiture", "car rental", "rent a car",
        "transport", "déplacement", "deplacement", "mileage", "km",
    ],
    "Meals": [
        "repas", "lunch", "dinner", "breakfast", "restaurant", "café", "cafe",
        "food", "snack", "dejeuner", "déjeuner", "diner", "dîner", "meal",
        "brasserie", "bistro", "pizza", "sushi", "buffet",
    ],
    "Accommodation": [
        "hotel", "hôtel", "airbnb", "hébergement", "hebergement",
        "nuit", "chambre", "logement", "séjour", "sejour", "inn", "motel",
    ],
    "Software": [
        "abonnement", "subscription", "saas", "software", "licence", "license",
        "aws", "gcp", "azure", "github", "slack", "figma", "notion", "zoom",
        "adobe", "microsoft", "google workspace", "digital",
    ],
    "Equipment": [
        "matériel", "materiel", "laptop", "phone", "equipment", "hardware",
        "écran", "ecran", "clavier", "keyboard", "mouse", "souris", "monitor",
        "imprimante", "printer", "cable", "chargeur", "charger",
    ],
}


def infer_category(description: str) -> str:
    desc_lower = description.lower()
    for category, keywords in CATEGORY_KEYWORDS.items():
        for kw in keywords:
            if kw in desc_lower:
                return category
    return "Other"


# ── Amount / currency parsing ─────────────────────────────────────────────────

AMOUNT_RE = re.compile(
    r'(?:'
    r'(\$|€|£|USD|EUR|GBP)\s*(\d[\d\s]*(?:[.,]\d{1,2})?)'   # prefix: $320
    r'|(\d[\d\s]*(?:[.,]\d{1,2})?)\s*(\$|€|£|USD|EUR|GBP)'   # suffix: 320€
    r')',
    re.IGNORECASE,
)

CURRENCY_CANONICAL = {
    "$": "$", "USD": "$",
    "€": "€", "EUR": "€",
    "£": "£", "GBP": "£",
}


def parse_amount(token: str):
    """Return (amount_float, currency_symbol) or (None, None)."""
    m = AMOUNT_RE.search(token)
    if not m:
        return None, None
    if m.group(1):
        sym = CURRENCY_CANONICAL.get(m.group(1).upper(), m.group(1))
        raw = m.group(2).replace(" ", "").replace(",", ".")
    else:
        sym = CURRENCY_CANONICAL.get(m.group(4).upper(), m.group(4))
        raw = m.group(3).replace(" ", "").replace(",", ".")
    try:
        return float(raw), sym
    except ValueError:
        return None, None


# ── Date extraction helper ────────────────────────────────────────────────────

DATE_RE = re.compile(
    r'\b(\d{4}-\d{2}-\d{2}'
    r'|\d{1,2}[/.\-]\d{1,2}[/.\-]\d{2,4}'
    r'|\d{1,2}\s+(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec'
    r'|jan|fév|mars|avr|mai|juin|juil|août|sep|oct|nov|déc)[a-z]*\.?\s*\d{2,4})\b',
    re.IGNORECASE,
)


def extract_line_date(line: str) -> str:
    m = DATE_RE.search(line)
    return m.group(1) if m else ""


# ── Period detection ──────────────────────────────────────────────────────────

PERIOD_RE = re.compile(
    r'(?:'
    r'(?:jan|fév|feb|mars|mar|avr|apr|mai|may|juin|jun|juil|jul|août|aug'
    r'|sep|oct|nov|déc|dec)[a-z]*\.?\s+\d{4}'       # "mars 2025"
    r'|Q[1-4]\s*\d{4}'                               # "Q1 2025"
    r'|\d{4}-\d{2}-\d{2}\s*(?:to|au|→|-)\s*\d{4}-\d{2}-\d{2}'  # date range
    r'|\d{1,2}/\d{1,2}/\d{4}\s*(?:to|au|→|-)\s*\d{1,2}/\d{1,2}/\d{4}'
    r')',
    re.IGNORECASE,
)


def get_period(txt: str) -> str:
    m = PERIOD_RE.search(txt[:800])
    return m.group(0).strip() if m else ""


# ── Core expense parser ───────────────────────────────────────────────────────

def parse_expenses(txt: str) -> list:
    """
    Returns list of dicts: {amount, currency, description, category, date}.
    Handles:
      - Pipe-delimited table: date | description | amount | category
      - Bullet lines: "- Travel: 450€" / "• Repas: 85.50 EUR"
      - Free-form: "Hotel Paris 2 nights: $320" / "Taxi 35€"
    """
    expenses = []

    for raw_line in txt.splitlines():
        line = raw_line.strip()
        if not line or len(line) < 3:
            continue
        # Skip header-ish lines
        if re.match(r'^(?:date|description|amount|montant|categ|total|===|---|\*\*\*)', line, re.IGNORECASE):
            continue

        amount, currency = None, None
        description = ""
        category = ""
        date = extract_line_date(line)

        # 1. Pipe-delimited: date | desc | amount | category (at least 3 pipes)
        if line.count("|") >= 2:
            parts = [p.strip() for p in line.split("|")]
            # Try to find amount among parts
            for i, part in enumerate(parts):
                a, c = parse_amount(part)
                if a is not None:
                    amount, currency = a, c
                    # Description: prefer part that is not date-like and not amount
                    for j, p in enumerate(parts):
                        if j != i and p and not DATE_RE.fullmatch(p.strip()) and not AMOUNT_RE.fullmatch(p.strip()):
                            description = p
                            break
                    # Category: last part if it doesn't contain amount
                    if len(parts) > i + 1:
                        last = parts[-1]
                        if not AMOUNT_RE.search(last):
                            category = last
                    break

        # 2. Bullet/labeled: "- Category: amount" or "• desc amount"
        if amount is None:
            # Strip bullet chars
            clean = re.sub(r'^[\-–•*]\s*', '', line)
            a, c = parse_amount(clean)
            if a is not None:
                amount, currency = a, c
                # Description: everything before the amount token
                desc_part = AMOUNT_RE.sub('', clean).strip().rstrip(':').strip()
                description = desc_part if desc_part else clean

        if amount is None:
            continue

        # Resolve description fallback
        if not description:
            description = AMOUNT_RE.sub('', line).strip()
            description = re.sub(r'^[\-–•*]\s*', '', description).strip().rstrip(':').strip()

        # Resolve category
        if not category:
            category = infer_category(description)

        expenses.append({
            "amount": amount,
            "currency": currency or "?",
            "description": description[:80] if description else "",
            "category": category,
            "date": date,
        })

    return expenses


# ── Anomaly detector ──────────────────────────────────────────────────────────

def detect_anomalies(expenses: list) -> list:
    anomalies = []
    seen_day_amounts: dict = defaultdict(list)

    for exp in expenses:
        desc = exp["description"]
        amt = exp["amount"]
        day = exp["date"]

        # Large single item > 500
        if amt > 500:
            label = desc if desc else f"{amt}{exp['currency']}"
            anomalies.append(f"Large expense: {label} ({_fmt(amt)}{exp['currency']})")

        # High meal cost > 100
        if exp["category"] == "Meals" and amt > 100:
            label = desc if desc else f"{amt}{exp['currency']}"
            anomalies.append(f"High meal: {label} ({_fmt(amt)}{exp['currency']})")

        # Missing description
        if not desc or len(desc.strip()) < 3:
            anomalies.append(f"Undescribed expense: {_fmt(amt)}{exp['currency']}")

        # Duplicate amounts on same day
        if day:
            key = (day, round(amt, 2))
            if key in seen_day_amounts:
                anomalies.append(f"Possible duplicate: {_fmt(amt)}{exp['currency']} on {day}")
            else:
                seen_day_amounts[key] = True

    return list(dict.fromkeys(anomalies))  # deduplicate preserving order


# ── Category summary ──────────────────────────────────────────────────────────

def summarize_by_category(expenses: list) -> dict:
    """Returns {category: {total, count}} sorted by total desc."""
    groups: dict = defaultdict(lambda: {"total": 0.0, "count": 0})
    for exp in expenses:
        groups[exp["category"]]["total"] += exp["amount"]
        groups[exp["category"]]["count"] += 1
    return dict(sorted(groups.items(), key=lambda kv: kv[1]["total"], reverse=True))


# ── Currency resolver ─────────────────────────────────────────────────────────

def get_currency(expenses: list) -> str:
    counts: dict = defaultdict(int)
    for exp in expenses:
        if exp["currency"] != "?":
            counts[exp["currency"]] += 1
    if not counts:
        return ""
    return max(counts, key=lambda k: counts[k])


# ── Amount formatter ──────────────────────────────────────────────────────────

def _fmt(amount: float) -> str:
    return str(int(amount)) if amount == int(amount) else f"{amount:.2f}"


# ── Main ──────────────────────────────────────────────────────────────────────

expenses = parse_expenses(text)

if not expenses:
    # Nothing parseable — pass through truncated
    print(text[:600])
    raise SystemExit

period   = get_period(text)
currency = get_currency(expenses)
total    = sum(e["amount"] for e in expenses)
by_cat   = summarize_by_category(expenses)
top5     = sorted(expenses, key=lambda e: e["amount"], reverse=True)[:5]
anomalies = detect_anomalies(expenses)

lines = ["=== BIBIVIDI: EXPENSE REPORT ==="]
if period:
    lines.append(f"Period  : {period}")
lines.append(f"Total   : {_fmt(total)}{currency}")
lines.append(f"Items   : {len(expenses)}")

lines.append("\n[BY CATEGORY]")
for cat, stats in by_cat.items():
    count_label = f"{stats['count']} item{'s' if stats['count'] != 1 else ''}"
    lines.append(f"  {cat:<16}: {_fmt(stats['total'])}{currency} ({count_label})")

if top5:
    lines.append("\n[TOP 5 ITEMS]")
    for exp in top5:
        desc = exp["description"] if exp["description"] else "(no description)"
        lines.append(f"  - {desc}: {_fmt(exp['amount'])}{exp['currency']}")

if anomalies:
    lines.append("\n[ANOMALIES]")
    for a in anomalies:
        lines.append(f"  - {a}")

lines.append(f"\n=== END (condensed from {len(text)} chars) ===")

print("\n".join(lines))
