#!/usr/bin/env python3
"""sku-number-check 0.1.0 — локальная сверка чисел черновика описания товара.

Запуск:  python3 check.py INPUT.json

Читает только указанный файл, печатает JSON в stdout. Сеть, ключи и запись
файлов не используются.

Коды выхода:
  0 — сигналов нет (это НЕ разрешение на публикацию);
  1 — есть сигналы: числа черновика не найдены в фактах и/или запретные фразы;
  2 — некорректный вход.
"""

import json
import re
import sys
import unicodedata
from decimal import Decimal, InvalidOperation

VERSION = "0.1.0"
MAX_INPUT_BYTES = 5 * 1024 * 1024
ALLOWED_KEYS = {"facts", "draft", "forbidden_phrases"}

# Последовательность цифр (любой системы Unicode, категория Nd), возможно
# с одной дробной частью через точку или запятую: 500, 0,5, 1.25, ٥٠٠, ５００.
NUMBER_RE = re.compile(r"\d+(?:[.,]\d+)?")

NO_SIGNALS_MESSAGE = (
    "Сигналов нет. Это не разрешение на публикацию: скрипт не проверяет смысл, "
    "единицы измерения и то, к какой характеристике относится число."
)
SIGNALS_MESSAGE = "Есть сигналы — проверьте их вручную по подтверждённым характеристикам."

LIMITATIONS = [
    "Сверяется только наличие числа в фактах, а не его смысл, единица измерения или поле.",
    "Число, совпавшее с любым числом из фактов, проходит, даже если оно относится к другой характеристике.",
    "Числа прописью («пятьсот») и разряды через пробел («1 000» читается как 1 и 000) не распознаются как одно число.",
    "Запретные фразы ищутся как точная форма слова без учёта регистра; другие словоформы и синонимы не ловятся.",
    "Новые качественные обещания без чисел и без заданных запретных фраз не ловятся.",
]


class InputError(Exception):
    pass


def to_ascii_digits(text):
    out = []
    for ch in text:
        if ch in ".,":
            out.append(".")
        else:
            out.append(str(unicodedata.digit(ch)))
    return "".join(out)


def normalize_number(token):
    """'0,50' -> '0.5', '٥٠٠' -> '500', '007' -> '7'."""
    # Canonicalize the lexical decimal without Decimal context rounding.
    value = to_ascii_digits(token)
    whole, _, fraction = value.partition(".")
    whole = whole.lstrip("0") or "0"
    fraction = fraction.rstrip("0")
    return whole + ("." + fraction if fraction else "")


def extract_numbers(text):
    return [
        {"token": m.group(0), "normalized": normalize_number(m.group(0)), "position": m.start()}
        for m in NUMBER_RE.finditer(text)
    ]


def find_phrase(text, phrase):
    """Позиции phrase в text без учёта регистра, только как отдельное слово/фраза."""
    folded_text = text.casefold()
    folded_phrase = phrase.casefold().strip()
    pattern = re.compile(r"(?<!\w)" + re.escape(folded_phrase) + r"(?!\w)")
    # casefold может менять длину строки (ß -> ss), поэтому позиции —
    # по свёрнутому тексту; для кириллицы и латиницы без лигатур они совпадают.
    return [m.start() for m in pattern.finditer(folded_text)]


def validate(data):
    if not isinstance(data, dict):
        raise InputError("корень JSON должен быть объектом")
    unknown = sorted(set(data) - ALLOWED_KEYS)
    if unknown:
        raise InputError("неизвестные поля: %s (допустимы: facts, draft, forbidden_phrases)" % ", ".join(unknown))
    facts = data.get("facts")
    if not isinstance(facts, list) or not facts:
        raise InputError("facts должен быть непустым списком строк")
    for i, item in enumerate(facts):
        if not isinstance(item, str) or not item.strip():
            raise InputError("facts[%d] должен быть непустой строкой" % i)
    draft = data.get("draft")
    if not isinstance(draft, str) or not draft.strip():
        raise InputError("draft должен быть непустой строкой")
    phrases = data.get("forbidden_phrases", [])
    if not isinstance(phrases, list):
        raise InputError("forbidden_phrases должен быть списком строк")
    for i, item in enumerate(phrases):
        if not isinstance(item, str) or not item.strip():
            raise InputError("forbidden_phrases[%d] должен быть непустой строкой" % i)
    return facts, draft, phrases


def check(data):
    facts, draft, phrases = validate(data)
    fact_values = set()
    for fact in facts:
        for num in extract_numbers(fact):
            fact_values.add(num["normalized"])
    draft_numbers = extract_numbers(draft)
    unmatched = [n for n in draft_numbers if n["normalized"] not in fact_values]
    forbidden_hits = []
    for phrase in phrases:
        for pos in find_phrase(draft, phrase):
            forbidden_hits.append({"phrase": phrase, "position": pos})
    has_signals = bool(unmatched or forbidden_hits)
    return {
        "tool": "sku-number-check",
        "version": VERSION,
        "status": "signals" if has_signals else "no_signals",
        "message": SIGNALS_MESSAGE if has_signals else NO_SIGNALS_MESSAGE,
        "unmatched_numbers": unmatched,
        "forbidden_hits": forbidden_hits,
        "draft_numbers": [n["normalized"] for n in draft_numbers],
        "fact_numbers": sorted(fact_values, key=lambda v: (Decimal(v), v)),
        "limitations": LIMITATIONS,
    }


def invalid(reason):
    return {
        "tool": "sku-number-check",
        "version": VERSION,
        "status": "invalid_input",
        "message": "Некорректный вход: %s" % reason,
    }


def main(argv):
    if len(argv) != 2:
        result, code = invalid("использование: python3 check.py INPUT.json"), 2
    else:
        try:
            with open(argv[1], "rb") as fh:
                raw = fh.read(MAX_INPUT_BYTES + 1)
            if len(raw) > MAX_INPUT_BYTES:
                raise InputError("файл больше 5 МБ")
            try:
                data = json.loads(raw.decode("utf-8"))
            except (UnicodeDecodeError, ValueError) as exc:
                raise InputError("файл не является JSON в UTF-8 (%s)" % exc.__class__.__name__)
            result = check(data)
            code = 1 if result["status"] == "signals" else 0
        except OSError as exc:
            result, code = invalid("не удалось прочитать файл (%s)" % exc.__class__.__name__), 2
        except InputError as exc:
            result, code = invalid(str(exc)), 2
    sys.stdout.write(json.dumps(result, ensure_ascii=False, indent=2) + "\n")
    return code


if __name__ == "__main__":
    sys.exit(main(sys.argv))
