"""
Veventa scoring, ported from the original engine.

Every number here came from the live app and is deliberately unchanged. The
weights, the bands, the base of 55, the plus or minus 40 on profit and
sell-through, the eight point penalty for a room not worked in three years:
all of it is twenty four years of event management expressed as arithmetic,
and none of it is mine to improve.

What has changed is where it runs. The score is computed from rows in a
database rather than from one encrypted blob, and it is deterministic: the
same event on the same data returns the same number every time, offline, with
no model call. The narrative that sits beside it still comes from GPT, because
that is the part a model is actually good at.
"""

from datetime import date


# Six bands, in order. Under 50 is a refusal, 90 and over is a night out.
BANDS = [
    (0,   50,  'BIG NO',          'crit'),
    (50,  60,  'Walking on Ice',  'warn'),
    (60,  70,  'Think Twice',     'warn'),
    (70,  80,  'Green Light',     'ok'),
    (80,  90,  'Good to Go',      'ok'),
    (90,  101, 'Enjoy the Vibe',  'ok'),
]

# The six drivers and what each is worth. They total 100.
FACTORS = [
    ('artist',    'Artist',    25),
    ('venue',     'Venue',     20),
    ('organiser', 'Organiser', 15),
    ('date',      'Date',      15),
    ('promo',     'Promotion', 15),
    ('market',    'Market',    10),
]


def clamp100(v):
    return max(0.0, min(100.0, float(v)))


def band_of(v):
    s = round(clamp100(v))
    for lo, hi, label, tone in BANDS:
        if lo <= s < hi:
            return {'score': s, 'label': label, 'tone': tone}
    return {'score': s, 'label': BANDS[-1][2], 'tone': BANDS[-1][3]}


def plural(n, word):
    return '%d %s%s' % (n, word, '' if n == 1 else 's')


class Driver(dict):
    """One bar on the assessment: a value, a confidence and the reasons."""

    def __init__(self, v, why=None, conf=0.0):
        super().__init__(v=clamp100(v), why=list(why or []), conf=float(conf))

    @property
    def v(self):
        return self['v']


def history(events, skip_id=None):
    """What a set of past dates says.

    Cancelled dates are left out: they tell you nothing about whether a room
    fills or an act draws. The event being assessed is excluded too, or it
    would be judging itself.
    """
    es = [e for e in events if e and e.pk != skip_id and e.status != 'cancelled']

    ticketed = [e for e in es if _alloc(e) > 0]
    sell = (sum(_sold(e) / _alloc(e) for e in ticketed) / len(ticketed)
            if ticketed else None)

    money = [e for e in es if _revenue(e) or _cost(e)]
    prof = (len([e for e in money if _pl(e) > 0]) / len(money)
            if money else None)

    years = [e.date.year for e in es if e.date]
    return {
        'n': len(es),
        'sell': sell,
        'prof': prof,
        'last': max(years) if years else None,
        'events': es,
    }


def from_history(h, base, noun):
    """Turn that history into a bar.

    Profit moves it forty points across its full range, sell-through the same,
    both measured against the point where they stop being a worry: half the
    dates profitable, sixty per cent of the house sold. Volume is worth up to
    ten. Confidence reaches one at five dates, because a judgement built on
    two is not a judgement.
    """
    why = []
    if not h['n']:
        return Driver(base, ['Nothing on file for %s yet.' % noun], 0.0)

    v = base + min(10, h['n'] * 2)
    why.append('%s on file.' % plural(h['n'], 'past date'))

    if h['prof'] is not None:
        v += (h['prof'] - 0.5) * 40
        why.append('%d%% of those finished in profit.' % round(h['prof'] * 100))

    if h['sell'] is not None:
        v += (h['sell'] - 0.6) * 40
        why.append('Average sell-through %d%%.' % round(h['sell'] * 100))

    age = date.today().year - (h['last'] or 0)
    if h['last'] and age > 3:
        v -= 8
        why.append('Last worked in %d, %d years ago.' % (h['last'], age))

    return Driver(v, why, min(1.0, h['n'] / 5.0))


# ── The figures a driver reads ──────────────────────────────────────────────
# Computed, never stored. MED is leaving spreadsheets behind and does not want
# a typed figure anywhere that a calculation could get wrong quietly.

def _alloc(event):
    return sum(t.allocation for t in event.tiers.all())


def _sold(event):
    return sum(t.sold for t in event.tiers.all())


def _revenue(event):
    """Tickets taken plus anything else that came in."""
    tickets = sum(t.price * t.sold for t in event.tiers.all())
    other = sum(i.amount for i in event.income.all())
    return float(tickets + other)


def _cost(event):
    return float(sum(b.total for b in event.budget.all()))


def _pl(event):
    return _revenue(event) - _cost(event) - float(event.box_office_charges or 0)


# ── The six drivers ─────────────────────────────────────────────────────────

def driver_artist(event, book):
    """The act is the biggest single thing, at a quarter of the score.

    The number shown is the act's own score from its page, unchanged, because
    that is the number people have already seen and arguing with it here would
    make the two disagree.
    """
    acts = [a for a in [event.main_act] if a] + list(event.featured.all())
    if not acts:
        return Driver(45, ['No act named, the biggest single driver is unknown.'], 0)

    main = acts[0]
    past = book.filter(artists=main)
    r = from_history(history(past, event.pk), 55, main.name)

    extra = ' with %d more billed.' % (len(acts) - 1) if len(acts) > 1 else '.'
    r['why'].insert(0, 'Main act: %s%s' % (main.name, extra))
    return r


def driver_venue(event, book):
    """The room, at a fifth. A tier plan larger than the room costs ten."""
    if not event.venue:
        return Driver(45, ['No venue chosen yet.'], 0)

    r = from_history(history(book.filter(venue=event.venue), event.pk),
                     55, event.venue.name)

    if event.venue.capacity:
        r['why'].insert(0, '%s holds %s.' % (event.venue.name,
                                             f'{event.venue.capacity:,}'))
        alloc = _alloc(event)
        if alloc and alloc > event.venue.capacity:
            r['v'] = clamp100(r['v'] - 10)
            r['why'].append('The tier plan puts %s on sale, above the room.'
                            % f'{alloc:,}')
    else:
        r['why'].insert(0, 'Capacity not recorded for %s.' % event.venue.name)

    return r


def driver_organiser(event, book):
    """Us, in this role, with this brand, and whoever is on it with us.

    Partners count for forty per cent of the bar when there are any, because
    we are not delivering it alone and their record is part of the risk.
    """
    same_role = book.filter(mode=event.mode)
    if event.brand and event.brand.company_id:
        same_role = same_role.filter(brand__company_id=event.brand.company_id)

    r = from_history(history(same_role, event.pk), 55, 'this role')
    r['why'].insert(0, 'Running it as %s.' % event.get_mode_display())

    if event.brand:
        hb = history(book.filter(brand=event.brand), event.pk)
        if hb['prof'] is not None:
            r['v'] = clamp100(r['v'] + (hb['prof'] - 0.5) * 20)
            r['why'].append('%s is %d%% profitable across %s.'
                            % (event.brand.name, round(hb['prof'] * 100),
                               plural(hb['n'], 'date')))

    scored = [(l.partner, l.partner.rating_avg) for l in event.partner_links.all()
              if l.partner and l.partner.rating_avg is not None]
    if scored:
        avg = round(sum(s for _, s in scored) / len(scored))
        r['v'] = clamp100(round(r['v'] * 0.6 + avg * 0.4))
        r['conf'] = max(r['conf'], 0.6)
        r['why'].append('%s on this date.'
                        % ', '.join('%s scores %s' % (p.name, round(s))
                                    for p, s in scored))
    return r


WEEKDAYS = ['Monday', 'Tuesday', 'Wednesday', 'Thursday',
            'Friday', 'Saturday', 'Sunday']
MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July',
          'August', 'September', 'October', 'November', 'December']


def driver_date(event, book, holidays=None, notes=None):
    """The night itself.

    Starts at 65 and is argued up or down: how this weekday and this month
    have actually performed against the average, your own dates clashing
    within three days, a big night that people go out for regardless, a bank
    holiday, and how long there is left to sell.
    """
    if not event.date:
        return Driver(45, ['No date fixed yet.'], 0)

    why, v, conf = [], 65.0, 0.4
    d = event.date
    all_dated = book.exclude(pk=event.pk).exclude(date=None).exclude(status='cancelled')

    overall = history(all_dated)

    same_dow = [e for e in all_dated if e.date.weekday() == d.weekday()]
    hw = history(same_dow)
    if hw['n'] >= 3 and hw['prof'] is not None and overall['prof'] is not None:
        v += (hw['prof'] - overall['prof']) * 60
        conf = 0.7
        why.append('%s dates run %d%% profitable against %d%% overall.'
                   % (WEEKDAYS[d.weekday()], round(hw['prof'] * 100),
                      round(overall['prof'] * 100)))

    same_month = [e for e in all_dated if e.date.month == d.month]
    hm = history(same_month)
    if hm['n'] >= 3 and hm['prof'] is not None and overall['prof'] is not None:
        v += (hm['prof'] - overall['prof']) * 40
        why.append('%s runs %d%% profitable across %s.'
                   % (MONTHS[d.month - 1], round(hm['prof'] * 100),
                      plural(hm['n'], 'date')))

    clash = [e for e in all_dated
             if e.city == event.city and abs((e.date - d).days) <= 3]
    if clash:
        v -= 10
        why.append('%s of your own within three days in %s.'
                   % (plural(len(clash), 'other date'), event.city))

    for h in (holidays or []):
        v += 6 if h.get('bank') else 2
        why.append(h['name'] + (', a bank holiday, so the night travels well.'
                                if h.get('bank') else '.'))

    for n in (notes or []):
        why.append('Your calendar note that day: %s.' % n)

    days = (d - date.today()).days
    if days >= 0:
        if days < 30:
            v -= 12
            why.append('%d days to doors, short for a full on-sale.' % days)
        elif days > 240:
            v -= 4
            why.append('%d days out, a long runway to hold attention.' % days)
        else:
            why.append('%d days to doors.' % days)
    else:
        why.append('The date has passed, so this reads as a post-mortem.')

    return Driver(v, why, conf)


PROMO_VERDICT = {'ok': 82, 'heavy': 70, 'thin': 48, 'none': 36, 'na': 45}


def driver_promo(event, assessment):
    """What is planned to sell it.

    The verdict maps to a number, then gaps and a short runway take points
    off. Three per missing channel, eight for not leaving a channel the lead
    time it needs.
    """
    verdict = assessment.get('verdict', 'na')
    v = PROMO_VERDICT.get(verdict, 50)
    why = [assessment.get('why', '')]
    conf = 0.1 if verdict == 'na' else 0.6

    gaps = assessment.get('gaps') or []
    if gaps:
        v -= len(gaps) * 3
        why.append('Nothing planned for %s.' % ', '.join(gaps))

    if assessment.get('lead_warning'):
        v -= 8
        why.append(assessment['lead_warning'])

    return Driver(v, [w for w in why if w], conf)


def driver_market(event, book):
    """Who could actually come.

    When the audience model is filled in it is the answer, scaled so that
    115% occupancy is full marks, because planning to sell more than the room
    is not better. Without it, fall back to how this city has performed.
    """
    aud = getattr(event, 'audience', None)
    if aud and aud.reachable and aud.total_capacity:
        occ = (aud.occupancy or 0) / 100.0
        v = clamp100(min(1.15, occ) / 1.15 * 100)
        return Driver(v, [
            'Audience model: %s reachable in %s.'
            % (f'{aud.reachable:,}', event.city or 'this market'),
            '%s likely to come against %s seats on sale (%s).'
            % (f'{aud.estimated_attendance or 0:,}',
               f'{aud.total_capacity:,}', plural(aud.shows, 'show')),
            '%d%% occupancy.' % round(occ * 100),
        ], 0.6)

    if not event.city:
        return Driver(50, ['No city set.'], 0)

    r = from_history(history(book.filter(city=event.city), event.pk), 55, event.city)
    r['why'].insert(0, 'Market: %s, %s.' % (event.city, event.country or ''))
    r['why'].append('Country and city economics are not in this record.')
    return r


def forecast(event, book=None, promo=None, holidays=None, notes=None):
    """The Veventa Score.

    Six drivers, weighted, nothing else. Confidence is weighted the same way,
    so a high score built on thin history reports itself as uncertain rather
    than pretending.

    `book` is the set of events this one is judged against, and it must be
    Event.objects.booked(): plans and cancellations say nothing about whether
    a room fills or an act draws.
    """
    from .models import Event
    if book is None:
        book = Event.objects.filter(tenant=event.tenant).booked()

    f = {
        'artist':    driver_artist(event, book),
        'venue':     driver_venue(event, book),
        'organiser': driver_organiser(event, book),
        'date':      driver_date(event, book, holidays, notes),
        'promo':     driver_promo(event, promo or {}),
        'market':    driver_market(event, book),
    }

    total = sum(f[k]['v'] * w for k, _, w in FACTORS) / 100.0
    conf = sum(f[k]['conf'] * w for k, _, w in FACTORS) / 100.0

    return {'drivers': f, 'band': band_of(total), 'score': round(total),
            'confidence': round(conf * 100)}
