"""One-to-one cited-line matching; no model judge or generated-code execution. MIT."""


def match(predictions, gold, owner=True):
    edges = [[j for j, g in enumerate(gold)
              if type(p.get('line')) is int and abs(p['line'] - g['line']) <= 2
              and (not owner or p.get('owner') == g['owner'])]
             for p in predictions]
    assigned = {}

    def augment(i, visited):
        for j in edges[i]:
            if j in visited:
                continue
            visited.add(j)
            if j not in assigned or augment(assigned[j], visited):
                assigned[j] = i
                return True
        return False

    for i in range(len(predictions)):
        augment(i, set())
    return {i: j for j, i in assigned.items()}


def score(item, arguments, config):
    actions = arguments.get('actions', [])
    decisions = arguments.get('decisions', [])
    if not isinstance(actions, list):
        actions = [{}]
    if not isinstance(decisions, list):
        decisions = [{}]
    actions = [p if isinstance(p, dict) else {} for p in actions]
    decisions = [p if isinstance(p, dict) else {} for p in decisions]
    gold = item['gold']['actions']
    gold_decisions = item['gold']['decisions']
    # Malformed entries cannot earn credit even if a line happens to match.
    clean = [p if set(p) == {'task', 'owner', 'due_date', 'line'} and isinstance(p['task'], str) and p['task'].strip()
             and (p['owner'] is None or isinstance(p['owner'], str))
             and (p['due_date'] is None or isinstance(p['due_date'], str)) else {} for p in actions]
    clean_decisions = [p if set(p) == {'decision', 'line'} and isinstance(p['decision'], str) and p['decision'].strip() else {} for p in decisions]
    pairs = match(clean, gold)
    decision_pairs = match(clean_decisions, gold_decisions, owner=False)
    counts = {'tp': len(pairs), 'fp': len(actions) - len(pairs), 'fn': len(gold) - len(pairs)}
    decision_counts = {'tp': len(decision_pairs), 'fp': len(decisions) - len(decision_pairs), 'fn': len(gold_decisions) - len(decision_pairs)}
    line_pairs = match(clean, gold, owner=False)
    invented = sum(p.get('due_date') is not None and (i not in line_pairs or p['due_date'] != gold[line_pairs[i]]['due_date']) for i, p in enumerate(actions))
    delegated = {j for j, g in enumerate(gold) if g['owner'] is not None and g['owner'] != g['speaker']}
    owner_correct = sum(j in delegated for j in pairs.values())
    return {**counts, 'secondary': {
        'action_precision': counts.copy(), 'action_recall': counts.copy(),
        'decision_precision': decision_counts.copy(), 'decision_recall': decision_counts.copy(),
        'owner_not_speaker_recall': {'tp': owner_correct, 'fp': 0, 'fn': len(delegated) - owner_correct},
        'invented_dates': {'score': invented},
        'invented_date_rate': {'tp': invented, 'fp': len(actions) - invented, 'fn': 0},
        'action_items_found': {'score': len(pairs)},
        'gold_action_items': {'score': len(gold)},
    }}
