#!/usr/bin/env python3
"""Объединение выгрузок по полю id с фиксацией дублей, конфликтов и пропусков.

Правила (заданы пользователем, угадывание запрещено):
  * совпавшие значения по одному id -> identical_duplicate;
  * расхождение значений -> conflict; значение берётся только по явному
    решению пользователя, иначе amount = null и status = conflict_unresolved;
  * id, присутствующий лишь в одной выгрузке -> one_sided;
  * отсутствующее значение поля сохраняется как null, не восстанавливается.

Решения по конфликтам принимаются человеком и передаются в CONFLICT_DECISIONS.
"""

import json
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
SRC_A = ROOT / "маршруты" / "данные" / "выгрузка-а.json"
SRC_B = ROOT / "маршруты" / "данные" / "выгрузка-б.json"

OUT_DATA = ROOT / "результаты" / "объединённые-маршруты.json"
OUT_JOURNAL = ROOT / "результаты" / "журнал-решений.md"

FIELD = "amount"

# Явные решения пользователя: id -> (значение, источник).
CONFLICT_DECISIONS = {
    "r3": (21, "выгрузка-б.json"),
}


def load(path):
    with path.open(encoding="utf-8") as fh:
        rows = json.load(fh)
    result = {}
    for row in rows:
        rid = row["id"]
        if rid in result:
            raise ValueError(f"{path.name}: повтор id={rid} внутри одного файла")
        result[rid] = row
    return result


def merge(a, b):
    records, journal = [], []
    for rid in sorted(set(a) | set(b), key=lambda x: (len(x), x)):
        in_a, in_b = rid in a, rid in b
        sources = [n for n, present in
                   (("выгрузка-а.json", in_a), ("выгрузка-б.json", in_b)) if present]

        if in_a and in_b:
            va, vb = a[rid].get(FIELD), b[rid].get(FIELD)
            if va == vb:
                records.append({"id": rid, FIELD: va,
                                "status": "identical_duplicate", "sources": sources})
                journal.append({
                    "id": rid, "kind": "identical_duplicate", "field": FIELD,
                    "значения": {"выгрузка-а.json": va, "выгрузка-б.json": vb},
                    "решение": "значения совпали, конфликта нет",
                })
            elif rid in CONFLICT_DECISIONS:
                value, source = CONFLICT_DECISIONS[rid]
                rejected = {"выгрузка-а.json": va, "выгрузка-б.json": vb}
                rejected.pop(source)
                records.append({
                    "id": rid, FIELD: value, "status": "conflict_resolved",
                    "sources": [source], "rejected_sources": list(rejected),
                })
                journal.append({
                    "id": rid, "kind": "conflict", "field": FIELD,
                    "значения": {"выгрузка-а.json": va, "выгрузка-б.json": vb},
                    "решение": f"принято значение {value!r} из {source}",
                    "отклонено": rejected,
                    "основание": "явное решение пользователя",
                })
            else:
                records.append({
                    "id": rid, FIELD: None, "status": "conflict_unresolved",
                    "sources": sources,
                    "conflicting_values": {"выгрузка-а.json": va,
                                           "выгрузка-б.json": vb},
                })
                journal.append({
                    "id": rid, "kind": "conflict", "field": FIELD,
                    "значения": {"выгрузка-а.json": va, "выгрузка-б.json": vb},
                    "решение": "не разрешён, значение не подставлялось",
                })
        else:
            row = a.get(rid) or b.get(rid)
            raw = row.get(FIELD)
            missing = FIELD not in row or raw is None
            records.append({
                "id": rid, FIELD: raw, "status": "one_sided",
                "sources": sources,
                "note": f"нет в {'выгрузка-б.json' if in_a else 'выгрузка-а.json'}"
                        + ("; значение отсутствует в источнике" if missing else ""),
            })
            journal.append({
                "id": rid, "kind": "gap", "field": FIELD,
                "присутствует": sources,
                "отсутствует": ["выгрузка-б.json" if in_a else "выгрузка-а.json"],
                "значение": raw,
                "решение": "значение отсутствует и не восстанавливалось"
                           if missing else "запись включена как есть",
            })
    return records, journal


def write_journal(journal, records):
    kinds = {"identical_duplicate": "Одинаковые дубли",
             "conflict": "Конфликты", "gap": "Пропуски"}
    lines = ["# Журнал решений: объединение выгрузок по полю `id`",
             "",
             "Источники: `маршруты/данные/выгрузка-а.json`, "
             "`маршруты/данные/выгрузка-б.json`. Поле объединения: `id`, "
             "сравниваемое поле: `amount`.",
             "",
             "Отсутствующие значения не восстанавливались. Конфликт `r3` разрешён "
             "пользователем: принято `21` из `выгрузка-б.json`, значение `20` из "
             "`выгрузка-а.json` отклонено.",
             ""]
    for kind, title in kinds.items():
        lines += [f"## {title}", "",
                  "| id | A | Б | Решение |", "| --- | --- | --- | --- |"]
        for entry in journal:
            if entry["kind"] != kind:
                continue
            if kind == "gap":
                shown = "null (отсутствует)" if entry["значение"] is None else entry["значение"]
                a_val = shown if "выгрузка-а.json" in entry["присутствует"] else "—"
                b_val = shown if "выгрузка-б.json" in entry["присутствует"] else "—"
            else:
                a_val = entry["значения"]["выгрузка-а.json"]
                b_val = entry["значения"]["выгрузка-б.json"]
            lines.append(f"| `{entry['id']}` | {a_val} | {b_val} | {entry['решение']} |")
        lines.append("")
    lines += ["## Итог", "",
              "| id | amount | status | источники |", "| --- | --- | --- | --- |"]
    for rec in records:
        lines.append(f"| `{rec['id']}` | {rec[FIELD]} | {rec['status']} | "
                     f"{', '.join(rec['sources'])} |")
    lines.append("")
    OUT_JOURNAL.write_text("\n".join(lines), encoding="utf-8")


def main():
    a, b = load(SRC_A), load(SRC_B)
    records, journal = merge(a, b)
    OUT_DATA.parent.mkdir(parents=True, exist_ok=True)
    OUT_DATA.write_text(json.dumps(records, ensure_ascii=False, indent=2) + "\n",
                        encoding="utf-8")
    write_journal(journal, records)
    print(json.dumps(records, ensure_ascii=False, indent=2))
    print(f"\nзаписано: {OUT_DATA.relative_to(ROOT)}")
    print(f"записано: {OUT_JOURNAL.relative_to(ROOT)}")


if __name__ == "__main__":
    main()
