
from __future__ import annotations
from pathlib import Path
import math
import re
from collections import Counter

__version__ = '4.14.0'

WORD_RE = re.compile(r'[A-Za-zА-Яа-яЁё0-9]+', re.U)


def tokenize(text: str):
    return WORD_RE.findall((text or '').lower())


def ngrams(tokens, n=3):
    if len(tokens) < n:
        return set()
    return set(tuple(tokens[i:i+n]) for i in range(len(tokens)-n+1))


def jaccard(a, b):
    if not a and not b:
        return 0.0
    return len(a & b) / max(1, len(a | b))


def grade5_from_100(score100: float, underwork: float, surplus: float) -> str:
    """Pedagogical five-point mapping. Plus/minus signs are used only here."""
    if score100 >= 96 and underwork < 8 and surplus < 8:
        return '5+'
    if score100 >= 86:
        return '5-' if underwork > 12 or surplus > 12 else '5'
    if score100 >= 76:
        return '4+'
    if score100 >= 66:
        return '4'
    if score100 >= 56:
        return '4-'
    if score100 >= 46:
        return '3+'
    if score100 >= 36:
        return '3'
    if score100 >= 26:
        return '3-'
    return '2'


def scale_pack(score100: float) -> dict:
    return {
        'score5': None,
        'score10': round(score100 / 10, 1),
        'score100': round(score100, 1),
        'score1000': round(score100 * 10),
        'score100000': round(score100 * 1000),
    }


def score_text(text: str, ideal: str = '', lang: str = 'ru') -> dict:
    toks = tokenize(text)
    ideal_toks = tokenize(ideal)
    length = len(toks)
    unique = len(set(toks))
    paragraphs = [x.strip() for x in (text or '').split('\n') if x.strip()]
    lower = set(toks)

    idea = min(100, 50 + min(length, 700) / 12)
    content = min(100, 35 + unique / 2.2)
    logic_words = {
        'ru': ['поэтому', 'следовательно', 'итак', 'значит', 'поскольку', 'однако', 'во-первых', 'во-вторых'],
        'en': ['therefore', 'hence', 'because', 'however', 'first', 'second', 'consequently'],
        'zh': ['因此', '所以', '但是', '首先', '其次']
    }.get(lang, [])
    logic = 65 + min(20, sum(1 for w in logic_words if w.lower() in lower) * 4)
    composition = 68 + min(20, len(paragraphs) * 2)
    clarity = 70 + (5 if 80 <= length <= 2500 else -3)

    ideal_score = 70
    ideal_jaccard = 0.0
    if ideal_toks:
        ideal_vocab = set(ideal_toks)
        text_vocab = set(toks)
        coverage = len(text_vocab & ideal_vocab) / max(1, len(ideal_vocab))
        ideal_jaccard = jaccard(ngrams(toks, 3), ngrams(ideal_toks, 3))
        ideal_score = min(100, 100 * (0.70 * coverage + 0.30 * min(1.0, ideal_jaccard * 5)))

    score100 = round(
        0.18 * idea + 0.22 * content + 0.20 * logic + 0.15 * composition + 0.10 * clarity + 0.15 * ideal_score,
        1,
    )
    underwork = max(0, round(100 - score100, 1))
    surplus = max(0, round(max(0, length - 700) / 22, 1))
    lam = round(-1 + underwork / 100 + surplus / 200, 4)
    delta = round(abs(lam + 1), 4)
    auth = round(1 / (1 + delta), 4)

    criteria = [
        ('Раскрытие темы / ведущая идея', round(idea, 1), 'Насколько ясно сформулирован и раскрыт главный тезис.'),
        ('Поле понятий / limU', round(idea, 1), 'Полнота понятийного поля относительно темы.'),
        ('Фактическое содержание / limR', round(content, 1), 'Достаточность фактов, примеров и содержательной опоры.'),
        ('Логика доказательства / limI', round(logic, 1), 'Связь тезисов, аргументов и вывода.'),
        ('Композиция и структура', round(composition, 1), 'Целостность, последовательность и завершенность текста.'),
        ('Ясность и стиль', round(clarity, 1), 'Понятность, точность и чистота изложения.'),
        ('Сходство с идеальным ответом', round(ideal_score, 1), 'Сопоставление с загруженной моделью отличного ответа.'),
    ]

    return {
        'U': round(idea / 100, 3),
        'I': round(logic / 100, 3),
        'R': round(content / 100, 3),
        'D': round(ideal_score / 100, 3),
        'lambda': lam,
        'delta': delta,
        'auth': auth,
        'score100': score100,
        'grade5': grade5_from_100(score100, underwork, surplus),
        'underwork': underwork,
        'surplus': surplus,
        'ideal_similarity': round(ideal_jaccard, 4),
        'criteria': criteria,
        **scale_pack(score100),
    }


def normalize_space(s: str) -> str:
    return re.sub(r'\s+', ' ', s or '').strip()


def normalize_class(number: str, letter: str) -> str:
    return f'{int(number)}-{letter.upper().replace("Ё", "Е")}'


def extract_metadata(text: str, filename: str = '') -> dict:
    lines = [ln.strip() for ln in (text or '').splitlines() if ln.strip()]
    head = '\n'.join(lines[:25])
    title = Path(filename).stem if filename else 'Без названия'

    author = None
    for pat in [r'(?:Автор|ФИО|Ученик|Ученица|Студент|Студентка)\s*[:\-]\s*([А-ЯЁ][а-яё]+(?:[ 	]+[А-ЯЁ][а-яё]+){0,1})',
                r'^([А-ЯЁ][а-яё]+(?:[ 	]+[А-ЯЁ][а-яё]+){1,2})']:
        m = re.search(pat, head, flags=re.I | re.M)
        if m:
            author = normalize_space(m.group(1))
            break
    if not author:
        author = 'Автор не указан'

    institution = None
    for pat in [r'(Школа\s*№?\s*\d+[^\n]*)', r'(Лицей\s*№?\s*\d+[^\n]*)',
                r'(Гимназия\s*№?\s*\d+[^\n]*)', r'(Колледж[^\n]*)',
                r'(Университет[^\n]*)', r'(Институт[^\n]*)']:
        m = re.search(pat, head, flags=re.I)
        if m:
            institution = normalize_space(m.group(1))
            institution = re.split(r'\s+(?:Класс|Группа|Автор|ФИО|Ученик|Ученица|Студент|Студентка)\b', institution, 1, flags=re.I)[0]
            break
    if not institution:
        institution = 'Учебное заведение не указано'

    group = None
    m = re.search(r'класс\s*[:\-]?\s*(\d{1,2})\s*[- ]?\s*([А-ЯЁA-Z])\b', head, flags=re.I)
    if m:
        group = normalize_class(m.group(1), m.group(2))
    else:
        m = re.search(r'\b(\d{1,2})\s*[- ]?\s*([А-ЯЁA-Z])\b', head, flags=re.I)
        if m and 1 <= int(m.group(1)) <= 11:
            group = normalize_class(m.group(1), m.group(2))
    if not group:
        m = re.search(r'группа\s*[:\-]?\s*([0-9A-Za-zА-Яа-яЁё\-]+)', head, flags=re.I)
        if m:
            group = 'Группа ' + m.group(1).upper()
    if not group:
        group = 'Класс/группа не указаны'

    return {'author': author, 'institution': institution, 'group': group, 'title': title}


def plagiarism_suspicion(text_a: str, text_b: str) -> dict:
    toks_a, toks_b = tokenize(text_a), tokenize(text_b)
    gram3 = jaccard(ngrams(toks_a, 3), ngrams(toks_b, 3))
    vocab = jaccard(set(toks_a), set(toks_b))
    len_ratio = min(len(toks_a), len(toks_b)) / max(1, max(len(toks_a), len(toks_b)))
    p = round(100 * (0.50 * gram3 + 0.35 * vocab + 0.15 * len_ratio), 1)
    if p >= 75:
        risk = 'Высокое подозрение'
    elif p >= 45:
        risk = 'Среднее подозрение'
    elif p >= 25:
        risk = 'Низкое подозрение'
    else:
        risk = 'Сходство несущественное'
    return {'suspicion_index': p, 'risk': risk, 'ngram3_jaccard': round(gram3, 4), 'vocab_jaccard': round(vocab, 4), 'length_ratio': round(len_ratio, 4)}
