"""
BSS Skill: Sales Pipeline Condenser
Parses a sales pipeline export (deals/opportunities as text or CSV-like) and
outputs total pipeline value, stage breakdown, top deals, stale/at-risk deals,
and win rate if closed data is present. Reduces pipeline reports by 75-90%.
"""
import csv
import io
import re
from collections import defaultdict
from datetime import date, datetime

text = PROMPT  # injected by BIBIVIDI sandbox wrapper

TODAY = date(2026, 3, 19)

# ── Currency detection ─────────────────────────────────────────────────────────

CURRENCY_RE = re.compile(r'(€|\$|£|EUR|USD|GBP)', re.IGNORECASE)

# Map lower-case symbol/code to canonical symbol
CURRENCY_CANON = {
    '€': '€', 'eur': '€',
    '$': '$', 'usd': '$',
    '£': '£', 'gbp': '£',
}


def get_currency(deals):
    counts = defaultdict(int)
    for d in deals:
        if d.get('currency'):
            counts[d['currency']] += 1
    if counts:
        return max(counts, key=counts.__getitem__)
    return '€'


# ── Amount parsing ─────────────────────────────────────────────────────────────

AMOUNT_RE = re.compile(
    r'(€|\$|£)?'
    r'\s*'
    r'(\d[\d\s,._]*)'
    r'\s*'
    r'(k|K|m|M)?'
    r'\s*'
    r'(€|\$|£|EUR|USD|GBP)?',
    re.IGNORECASE,
)


def parse_amount(token):
    """
    Return (float_amount, currency_symbol) or (None, None).
    Handles: €50,000  $50k  50k€  1.5M  50 000
    """
    m = AMOUNT_RE.search(token)
    if not m:
        return None, None
    pre_sym, num_raw, multiplier, post_sym = m.groups()

    # Must have at least one digit
    if not re.search(r'\d', num_raw):
        return None, None

    sym_raw = pre_sym or post_sym or ''
    sym = CURRENCY_CANON.get(sym_raw.lower(), sym_raw) if sym_raw else ''

    # Clean number: remove spaces, dots-as-thousands, keep decimal comma
    num_clean = re.sub(r'[\s_]', '', num_raw)
    # Detect thousands separator style
    if re.match(r'^\d{1,3}(\.\d{3})+(,\d+)?$', num_clean):
        # European: 1.234.567,89
        num_clean = num_clean.replace('.', '').replace(',', '.')
    elif re.match(r'^\d{1,3}(,\d{3})+(\.\d+)?$', num_clean):
        # US: 1,234,567.89
        num_clean = num_clean.replace(',', '')
    else:
        num_clean = num_clean.replace(',', '.')

    try:
        amount = float(num_clean)
    except ValueError:
        return None, None

    if multiplier and multiplier.lower() == 'k':
        amount *= 1_000
    elif multiplier and multiplier.lower() == 'm':
        amount *= 1_000_000

    return amount, sym


# ── Stage normalisation ────────────────────────────────────────────────────────

STAGE_MAP = [
    ('Closed Won',   re.compile(r'\b(won|gagn[eé]|closed\s*won|win|sign[eé])\b', re.I)),
    ('Closed Lost',  re.compile(r'\b(lost|perdu|closed\s*lost)\b', re.I)),
    ('Negotiation',  re.compile(r'\b(negoc|contract|closing|signature)\b', re.I)),
    ('Proposal',     re.compile(r'\b(proposal|devis|offre|quote|soumission)\b', re.I)),
    ('Qualification',re.compile(r'\b(qualif|disco|discovery|qualify)\b', re.I)),
    ('Prospecting',  re.compile(r'\b(prospect|lead|new|d[eé]couverte|cold)\b', re.I)),
]


def normalize_stage(raw):
    if not raw:
        return raw
    for canonical, pattern in STAGE_MAP:
        if pattern.search(raw):
            return canonical
    return raw.strip().title()


# ── Date parsing ───────────────────────────────────────────────────────────────

MONTH_MAP = {
    'jan': 1, 'feb': 2, 'fév': 2, 'mar': 3, 'apr': 4, 'avr': 4,
    'may': 5, 'mai': 5, 'jun': 6, 'juin': 6, 'jul': 7, 'juil': 7,
    'aug': 8, 'aoû': 8, 'sep': 9, 'oct': 10, 'nov': 11, 'dec': 12, 'déc': 12,
}


def parse_date(token):
    """Return a date object or None."""
    token = token.strip()
    # ISO: 2026-04-15
    m = re.match(r'^(\d{4})-(\d{2})-(\d{2})$', token)
    if m:
        try:
            return date(int(m.group(1)), int(m.group(2)), int(m.group(3)))
        except ValueError:
            pass
    # DD/MM/YYYY or MM/DD/YYYY (ambiguous — assume DD/MM/YYYY for European context)
    m = re.match(r'^(\d{1,2})[/.\-](\d{1,2})[/.\-](\d{2,4})$', token)
    if m:
        d, mo, yr = int(m.group(1)), int(m.group(2)), int(m.group(3))
        if yr < 100:
            yr += 2000
        try:
            return date(yr, mo, d)
        except ValueError:
            try:
                return date(yr, d, mo)
            except ValueError:
                pass
    # "15 April 2026" / "April 15 2026" / "15 avr 2026"
    m = re.match(
        r'^(\d{1,2})\s+([a-zéû]{3,})\s+(\d{4})$', token, re.IGNORECASE,
    )
    if not m:
        m = re.match(
            r'^([a-zéû]{3,})\s+(\d{1,2})\s+(\d{4})$', token, re.IGNORECASE,
        )
        if m:
            month_str, day, year = m.group(1), int(m.group(2)), int(m.group(3))
            mon = MONTH_MAP.get(month_str[:3].lower())
            if mon:
                try:
                    return date(year, mon, day)
                except ValueError:
                    pass
    if m and m.lastindex == 3:
        day_g, month_str, year = m.group(1), m.group(2), m.group(3)
        mon = MONTH_MAP.get(month_str[:3].lower())
        if mon:
            try:
                return date(int(year), mon, int(day_g))
            except ValueError:
                pass
    return None


def find_date_in_token(token):
    """Search for a date pattern inside a larger token."""
    for pat in [
        r'\d{4}-\d{2}-\d{2}',
        r'\d{1,2}[/.\-]\d{1,2}[/.\-]\d{2,4}',
        r'\d{1,2}\s+[a-zéû]{3,}\s+\d{4}',
        r'[a-zéû]{3,}\s+\d{1,2}\s+\d{4}',
    ]:
        m = re.search(pat, token, re.IGNORECASE)
        if m:
            d = parse_date(m.group(0))
            if d:
                return d, m.group(0)
    return None, ''


# ── Deal parsing ───────────────────────────────────────────────────────────────

def _build_deal(name='', stage='', amount=None, currency='', close_date='', owner=''):
    return {
        'name': name.strip()[:80],
        'stage': normalize_stage(stage),
        'amount': amount,
        'currency': currency,
        'close_date': close_date,
        'owner': owner.strip()[:40],
    }


def _try_parse_csv(txt):
    """Try to parse as CSV (comma or semicolon delimited)."""
    # Detect delimiter
    first_line = txt.splitlines()[0] if txt.splitlines() else ''
    delimiter = ';' if first_line.count(';') > first_line.count(',') else ','

    try:
        reader = csv.DictReader(io.StringIO(txt), delimiter=delimiter)
        rows = list(reader)
        headers = reader.fieldnames or []
    except Exception:
        return []

    if not rows or not headers or len(rows) < 1:
        return []

    # Map header names to fields
    def find_col(patterns):
        for pat in patterns:
            for h in headers:
                if re.search(pat, h, re.IGNORECASE):
                    return h
        return None

    col_name   = find_col([r'company|deal|name|account|client|opportunit|nom'])
    col_stage  = find_col([r'stage|étape|status|statut|phase'])
    col_amount = find_col([r'amount|value|valeur|revenue|montant|price|prix'])
    col_date   = find_col([r'close\s*date|closing|date|échéance|deadline'])
    col_owner  = find_col([r'owner|rep|sales\s*rep|assigned|commercial|responsable'])

    if not col_name and not col_amount:
        return []

    deals = []
    for row in rows:
        name   = row.get(col_name, '').strip() if col_name else ''
        stage  = row.get(col_stage, '').strip() if col_stage else ''
        amt_raw = row.get(col_amount, '').strip() if col_amount else ''
        date_raw = row.get(col_date, '').strip() if col_date else ''
        owner  = row.get(col_owner, '').strip() if col_owner else ''

        amount, currency = parse_amount(amt_raw) if amt_raw else (None, '')
        # Try to get currency from amount column header or value
        if not currency:
            _, currency = parse_amount(col_amount or '')

        close_date = date_raw
        if not name and not amount:
            continue
        deals.append(_build_deal(name, stage, amount, currency, close_date, owner))

    return deals


def _try_parse_markdown_table(txt):
    """Parse a markdown table: | Company | Stage | Amount | ..."""
    lines = [l.strip() for l in txt.splitlines() if l.strip().startswith('|')]
    if len(lines) < 2:
        return []

    # First line = headers, second line = separator, rest = rows
    def split_row(line):
        return [c.strip() for c in line.strip('|').split('|')]

    headers = split_row(lines[0])
    data_lines = [l for l in lines[1:] if not re.match(r'^\|[\s\-:|]+\|', l)]

    def find_col(patterns):
        for pat in patterns:
            for i, h in enumerate(headers):
                if re.search(pat, h, re.IGNORECASE):
                    return i
        return None

    ci_name   = find_col([r'company|deal|name|account|client|opportunit|nom'])
    ci_stage  = find_col([r'stage|étape|status|statut|phase'])
    ci_amount = find_col([r'amount|value|valeur|revenue|montant|price|prix'])
    ci_date   = find_col([r'close\s*date|closing|date|échéance'])
    ci_owner  = find_col([r'owner|rep|sales\s*rep|commercial|responsable'])

    if ci_name is None and ci_amount is None:
        return []

    deals = []
    for line in data_lines:
        cells = split_row(line)
        def cell(i):
            return cells[i].strip() if i is not None and i < len(cells) else ''

        name   = cell(ci_name)
        stage  = cell(ci_stage)
        amt_raw = cell(ci_amount)
        date_raw = cell(ci_date)
        owner  = cell(ci_owner)

        amount, currency = parse_amount(amt_raw) if amt_raw else (None, '')
        if not name and not amount:
            continue
        deals.append(_build_deal(name, stage, amount, currency, date_raw, owner))

    return deals


def _try_parse_bullets(txt):
    """
    Bullet list format:
      - Acme Corp: €50,000 — Proposal — close: 2026-04-15
      • Acme Corp (Proposal) 50k€ - John - 15 April 2026
    """
    deals = []
    for line in txt.splitlines():
        line = line.strip()
        if not re.match(r'^[-•*]\s', line):
            continue
        line = re.sub(r'^[-•*]\s+', '', line)

        # Extract company name (before first separator)
        name_m = re.match(r'^([^:,|—\-\(]{3,40})[\s:,|—\-]', line)
        name = name_m.group(1).strip() if name_m else ''

        # Extract amount
        amount, currency = None, ''
        for token in re.split(r'[\s—\-|,;]+', line):
            a, c = parse_amount(token)
            if a is not None:
                amount, currency = a, c
                break

        # Extract stage
        stage = ''
        for canonical, pattern in STAGE_MAP:
            if pattern.search(line):
                stage = canonical
                break

        # Extract date
        close_date = ''
        date_m = re.search(
            r'(?:close\s*[:\-]?\s*|closing\s*[:\-]?\s*|date\s*[:\-]?\s*)'
            r'(\d{4}-\d{2}-\d{2}|\d{1,2}[/.\-]\d{1,2}[/.\-]\d{2,4}'
            r'|\d{1,2}\s+[a-zéû]{3,}\s+\d{4})',
            line, re.IGNORECASE,
        )
        if date_m:
            close_date = date_m.group(1)
        else:
            d_obj, d_str = find_date_in_token(line)
            if d_obj:
                close_date = d_str

        # Extract owner — last capitalized word/name after a separator
        owner = ''
        owner_m = re.search(
            r'[\-—|]\s*([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)\s*$', line,
        )
        if owner_m:
            candidate = owner_m.group(1)
            # Don't steal stage names
            if not any(candidate.lower() in s.lower() for s, _ in STAGE_MAP):
                owner = candidate

        if not name and not amount:
            continue
        deals.append(_build_deal(name, stage, amount, currency, close_date, owner))

    return deals


def parse_deals(txt):
    """Try CSV → markdown table → bullet list. Return best result."""
    # Try CSV (must have header row with commas/semicolons)
    csv_lines = [l for l in txt.splitlines() if l.count(',') >= 2 or l.count(';') >= 2]
    if len(csv_lines) >= 3:
        deals = _try_parse_csv('\n'.join(csv_lines))
        if deals:
            return deals

    # Try markdown table
    md_lines = [l for l in txt.splitlines() if l.strip().startswith('|')]
    if len(md_lines) >= 3:
        deals = _try_parse_markdown_table(txt)
        if deals:
            return deals

    # Bullet list
    deals = _try_parse_bullets(txt)
    if deals:
        return deals

    # Last resort: try CSV on full text
    deals = _try_parse_csv(txt)
    return deals


# ── Aggregation helpers ────────────────────────────────────────────────────────

def get_stage_summary(deals):
    """Return list of (stage, total_amount, count) sorted by total desc."""
    groups = defaultdict(lambda: {'total': 0.0, 'count': 0})
    for d in deals:
        s = d['stage'] or 'Unknown'
        groups[s]['count'] += 1
        if d['amount'] is not None:
            groups[s]['total'] += d['amount']
    return sorted(
        [(s, g['total'], g['count']) for s, g in groups.items()],
        key=lambda x: x[1],
        reverse=True,
    )


def get_top_deals(deals, n=5):
    with_amount = [d for d in deals if d['amount'] is not None]
    return sorted(with_amount, key=lambda d: d['amount'], reverse=True)[:n]


def get_stale_deals(deals, txt):
    """
    Flag stale deals:
    1. If "last activity" dates found near deal names, flag those > 30 days old.
    2. Otherwise, flag deals whose close_date is in the past.
    Max 5 stale deals returned.
    """
    # Try to find last-activity dates in the text
    activity_re = re.compile(
        r'(?:last\s*(?:activity|contact|update|touch)|dernière?\s*(?:activité|contact|mise\s*à\s*jour))'
        r'\s*[:\-]?\s*'
        r'(\d{4}-\d{2}-\d{2}|\d{1,2}[/.\-]\d{1,2}[/.\-]\d{2,4}'
        r'|\d{1,2}\s+[a-zéû]{3,}\s+\d{4})',
        re.IGNORECASE,
    )
    # Build a rough map: deal name → last activity date
    stale = []
    activity_matches = list(activity_re.finditer(txt))

    if activity_matches:
        # Associate each activity date with the nearest preceding deal name
        name_positions = {}
        for d in deals:
            if d['name']:
                pos = txt.lower().find(d['name'].lower())
                if pos >= 0:
                    name_positions[d['name']] = pos

        for am in activity_matches:
            date_obj = parse_date(am.group(1))
            if not date_obj:
                continue
            days_ago = (TODAY - date_obj).days
            if days_ago > 30:
                # Find closest preceding deal name
                closest_name = None
                closest_dist = float('inf')
                for name, pos in name_positions.items():
                    dist = am.start() - pos
                    if 0 <= dist < closest_dist:
                        closest_dist = dist
                        closest_name = name
                if closest_name:
                    for d in deals:
                        if d['name'] == closest_name and d not in stale:
                            stale.append(d)
                            break
    else:
        # Fallback: close_date in the past
        for d in deals:
            if d['stage'] in ('Closed Won', 'Closed Lost'):
                continue
            if d['close_date']:
                date_obj, _ = find_date_in_token(d['close_date'])
                if date_obj and date_obj < TODAY:
                    stale.append(d)

    return stale[:5]


def get_win_rate(deals):
    won  = sum(1 for d in deals if d['stage'] == 'Closed Won')
    lost = sum(1 for d in deals if d['stage'] == 'Closed Lost')
    total_closed = won + lost
    if total_closed == 0:
        return None
    return round(won / total_closed * 100, 1)


def get_total(deals):
    return sum(d['amount'] for d in deals if d['amount'] is not None and d['stage'] != 'Closed Lost')


# ── Amount formatting ──────────────────────────────────────────────────────────

def fmt_amount(amount, currency):
    if amount is None:
        return 'N/A'
    if amount == int(amount):
        return f'{int(amount):,}{currency}'
    return f'{amount:,.2f}{currency}'


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

deals = parse_deals(text)

if not deals:
    # Nothing parseable — pass through truncated prompt
    print(text[:800])
    raise SystemExit

currency    = get_currency(deals)
total       = get_total(deals)
stage_summary = get_stage_summary(deals)
top_deals   = get_top_deals(deals)
stale       = get_stale_deals(deals, text)
win_rate    = get_win_rate(deals)

lines = ['=== BIBIVIDI: SALES PIPELINE ===']
lines.append(f'Total    : {fmt_amount(total, currency)} ({len(deals)} deals)')
if win_rate is not None:
    lines.append(f'Win rate : {win_rate}%')

lines.append('\n[BY STAGE]')
for stage, s_total, count in stage_summary:
    lines.append(f'  {stage:<14}: {fmt_amount(s_total, currency)} ({count} deal{"s" if count != 1 else ""})')

if top_deals:
    lines.append('\n[TOP DEALS]')
    for d in top_deals:
        parts = [d['name'] or '(unnamed)']
        if d['stage']:
            parts.append(d['stage'])
        parts.append(fmt_amount(d['amount'], d['currency'] or currency))
        if d['owner']:
            parts.append(d['owner'])
        lines.append(f'  - {" | ".join(parts)}')

if stale:
    lines.append('\n[STALE / AT RISK]')
    for d in stale:
        parts = [d['name'] or '(unnamed)']
        if d['stage']:
            parts.append(d['stage'])
        parts.append(fmt_amount(d['amount'], d['currency'] or currency))
        if d['close_date']:
            parts.append(f'close: {d["close_date"]}')
        lines.append(f'  - {" | ".join(parts)}')

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

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