"""Laboratoire fictif, sans modèle, moteur vectoriel, API ni données client.

Les vecteurs 2D sont écrits à la main. Le reste contrôle des métadonnées
locales : il ne génère pas d'embeddings pour le corpus documentaire.
"""
from copy import deepcopy
from math import sqrt


def cosine(a, b):
    if len(a) != len(b):
        raise ValueError("dimensions incompatibles")
    return sum(x*y for x, y in zip(a, b)) / sqrt(sum(x*x for x in a)*sum(y*y for y in b))


def rank(query, documents):
    return sorted(documents, key=lambda key: (-cosine(query, documents[key]), key))


def checked_rank(query, documents, query_profile, index_profile):
    if query_profile != index_profile:
        raise ValueError("profils incompatibles")
    return rank(query, documents)


def record(revision, body, readers, sequence, deleted=False):
    return dict(revision=revision, body=body, readers=tuple(readers),
                sequence=sequence, deleted=deleted)


def apply(state, key, value):
    if key in state and value['sequence'] <= state[key]['sequence']:
        return False
    state[key] = deepcopy(value)
    return True


def manifest(state):
    return {key: (value['revision'], value['body'], value['readers'])
            for key, value in state.items() if not value['deleted']}


def readable(state, reader):
    return sorted(key for key, value in state.items()
                  if not value['deleted'] and reader in value['readers'])


def structural_gate(candidate, source, reached, required):
    return reached >= required and manifest(candidate) == manifest(source)


def exercise():
    checks = 0
    def check(condition, label):
        nonlocal checks
        assert condition, label
        checks += 1

    vectors_a = {'D01': (1, 0), 'D02': (0, 1)}
    vectors_b = {'D01': (0, 1), 'D02': (-1, 0)}
    qa, qb = (1, 0), (0, 1)
    check(checked_rank(qa, vectors_a, 'A', 'A')[0] == 'D01', 'couple A/A')
    check(checked_rank(qb, vectors_b, 'B', 'B')[0] == 'D01', 'couple B/B')
    check(rank(qb, vectors_a)[0] == 'D02', 'melange non protege')
    try:
        checked_rank(qb, vectors_a, 'B', 'A')
    except ValueError:
        checks += 1
    else:
        raise AssertionError('melange accepte')
    try:
        cosine((1, 0, 0), (1, 0))
    except ValueError:
        checks += 1
    else:
        raise AssertionError('dimensions acceptees')

    initial = {
        'D01': record(1, 'Procedure retour', ['alice', 'finance'], 40),
        'D02': record(1, 'Conditions remise', ['alice', 'finance'], 40),
        'D03': record(1, 'Ancienne procedure', ['alice'], 40),
        'D04': record(1, 'Tarif 100', ['alice', 'finance'], 40),
    }
    changes = [
        ('D03', record(2, '', [], 41, deleted=True)),
        ('D04', record(2, 'Tarif 120', ['alice', 'finance'], 42)),
        ('D05', record(1, 'Nouvelle procedure', ['alice', 'finance'], 43)),
        ('D02', record(2, 'Conditions remise', ['finance'], 44)),
    ]
    source = deepcopy(initial)
    for key, value in changes:
        apply(source, key, value)
    candidate = deepcopy(initial)
    check(len(manifest(candidate)) == len(manifest(source)) == 4, 'compteur trompeur')
    check(set(manifest(candidate)) != set(manifest(source)), 'identites differentes')
    check(not structural_gate(candidate, source, 44, 44), 'compteur seul insuffisant')
    check(not structural_gate(source, source, 43, 44), 'retard de journal refuse')
    for key, value in changes:
        apply(candidate, key, value)
    check(structural_gate(candidate, source, 44, 44), 'metadonnees rapprochees')
    check(readable(candidate, 'alice') == ['D01', 'D04', 'D05'], 'droits Alice')
    check(readable(candidate, 'finance') == ['D01', 'D02', 'D04', 'D05'], 'droits finance')
    check(not apply(candidate, 'D04', initial['D04']), 'ancienne revision refusee')
    check(candidate['D04']['body'] == 'Tarif 120', 'contenu actuel conserve')
    check(not apply(candidate, 'D03', initial['D03']), 'resurrection refusee')
    check('D03' not in manifest(candidate), 'retrait conserve')
    check(not structural_gate(initial, source, 40, 44), 'repli ancien refuse')
    fallback = deepcopy(initial)
    for key, value in changes:
        apply(fallback, key, value)
    check(structural_gate(fallback, source, 44, 44), 'repli synchronise')
    print(f'{checks} controles reussis. A/A et B/B : D01 ; melange B/A non protege : D02.')
    print('Deux inventaires de 4 documents differents ; apres rapprochement : Alice D01,D04,D05.')
    print('Aucun embedding appris, index distribue, LLM ou appel externe execute.')


if __name__ == '__main__':
    exercise()
