"""Мінімальний клієнт обміну ApexSFA ↔ облікова система (приклад).

Залежність: requests (pip install requests).
Змінні середовища:
    APEX_BASE_URL   — напр. https://api.apexsfa.com/api/v1
    APEX_TENANT_ID  — UUID тенанта (точки синхронізації)
    APEX_SECRET     — ключ обміну тенанта

Запуск:
    python exchange_client.py send   # відправити тестовий батч довідників
    python exchange_client.py pull   # забрати документи з черги й підтвердити
"""

from __future__ import annotations

import hashlib
import json
import os
import sys
import time
import uuid
from collections.abc import Callable
from typing import Any

import requests

BASE_URL = os.environ.get("APEX_BASE_URL", "https://api.apexsfa.com/api/v1").rstrip("/")
TENANT_ID = os.environ["APEX_TENANT_ID"]
SECRET = os.environ["APEX_SECRET"]

BATCH_CHUNK = 1000  # сервер приймає до 5000 конвертів, але менші пакети надійніші


def sign_v2(body: str, secret: str, ts: int | None = None) -> str:
    """X-Signature схеми v2: sha256(ts + "." + body + "." + secret) у hex."""
    ts = int(time.time()) if ts is None else ts
    digest = hashlib.sha256(f"{ts}.{body}.{secret}".encode()).hexdigest()
    return f"t={ts},v2={digest}"


def _post(
    path: str, payload: dict[str, Any], *, max_429_retries: int = 4
) -> requests.Response:
    # Тіло серіалізуємо один раз: підпис рахується саме з тих байтів, що підуть у мережу.
    body = json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
    for attempt in range(max_429_retries + 1):
        headers = {
            "Content-Type": "application/json",
            "X-Tenant-Id": TENANT_ID,
            "X-Signature": sign_v2(body, SECRET),
        }
        resp = requests.post(
            f"{BASE_URL}{path}", data=body.encode("utf-8"), headers=headers, timeout=300
        )
        if resp.status_code != 429 or attempt == max_429_retries:
            return resp
        pause = int(resp.headers.get("Retry-After", "5") or 5)
        time.sleep(max(1, min(pause, 60)))
    return resp


def send_batch(
    items: list[dict[str, Any]], *, partial: bool = True
) -> list[dict[str, Any]]:
    """Відправити конверти пакетами по BATCH_CHUNK. Повертає результати всіх рядків."""
    all_results: list[dict[str, Any]] = []
    for start in range(0, len(items), BATCH_CHUNK):
        chunk = items[start : start + BATCH_CHUNK]
        resp = _post("/webhooks/1c/inbound/batch", {"partial": partial, "items": chunk})
        if resp.status_code != 200:
            raise RuntimeError(
                f"batch {start}: HTTP {resp.status_code} {resp.text[:500]}"
            )
        data = resp.json()["data"]
        for row in data["results"]:
            row["index"] += start
            if not row["accepted"]:
                print(
                    f"  ✗ {row['entity_type']} {row['external_id']}: {row.get('error')}"
                )
        all_results.extend(data["results"])
        print(
            f"пакет {start}: прийнято {data['accepted_count']}, відхилено {data['failed_count']}"
        )
    return all_results


def pull_and_ack(
    handler: Callable[[dict[str, Any]], str | None],
    *,
    entity_types: list[str] | None = None,
    batch_size: int = 100,
    max_rounds: int = 20,
) -> None:
    """Забрати елементи черги, обробити кожен handler-ом і підтвердити.

    handler(item) повертає ідентифікатор створеного документа (або None)
    чи кидає виняток. Виняток з текстом, що починається на "[PERM]", —
    остаточна відмова без повторів.
    """
    for _ in range(max_rounds):
        req: dict[str, Any] = {"batch_size": batch_size}
        if entity_types:
            req["entity_types"] = entity_types
        # Однакове тіло в ту саму секунду дасть однаковий підпис, а повтор
        # підпису сервер відхиляє (антиповтор) — тому унікальна мітка.
        req["request_nonce"] = str(uuid.uuid4())
        resp = _post("/webhooks/1c/outbox/pull", req)
        if resp.status_code != 200:
            raise RuntimeError(f"pull: HTTP {resp.status_code} {resp.text[:500]}")
        data = resp.json()["data"]
        items = data["items"]
        if not items:
            print("черга порожня")
            return

        results: list[dict[str, Any]] = []
        for item in items:
            try:
                external_id = handler(item)
                res: dict[str, Any] = {"queue_id": item["queue_id"], "status": "ok"}
                if external_id:
                    res["external_id"] = str(external_id)[:200]
            except Exception as exc:
                message = str(exc)[:2000] or exc.__class__.__name__
                res = {
                    "queue_id": item["queue_id"],
                    "status": "error",
                    "error_message": message,
                    "permanent": message.startswith("[PERM]"),
                }
            results.append(res)

        for start in range(0, len(results), 200):
            ack = _post(
                "/webhooks/1c/outbox/ack", {"results": results[start : start + 200]}
            )
            if ack.status_code != 200:
                # Без ACK елементи повернуться у видачу самі після тайм-ауту блокування.
                raise RuntimeError(f"ack: HTTP {ack.status_code} {ack.text[:500]}")
            print("ack:", ack.json()["data"])

        if not data.get("has_more"):
            return


def _demo_handler(item: dict[str, Any]) -> str | None:
    payload = item["payload"]
    if item["entity_type"] == "order":
        print(
            f"заявка {payload.get('order_number')} ({item['operation']}), рядків {len(payload.get('lines') or [])}"
        )
        return f"DOC-{payload.get('order_number')}"
    if item["entity_type"] == "order_payment":
        print(
            f"оплата {payload.get('payment_number')} на {payload.get('amount')} {payload.get('currency')}"
        )
        return f"PKO-{payload.get('payment_number')}"
    raise RuntimeError(
        f"[PERM] тип {item['entity_type']} цим клієнтом не підтримується"
    )


if __name__ == "__main__":
    command = sys.argv[1] if len(sys.argv) > 1 else "pull"
    if command == "send":
        send_batch(
            [
                {
                    "entity_type": "warehouse",
                    "operation": "upsert",
                    "external_id": "8d3b4c5e-6f70-11ee-9a0b-0050569a1b2c",
                    "payload": {"name": "Склад Основний"},
                }
            ]
        )
    elif command == "pull":
        pull_and_ack(_demo_handler, entity_types=["order", "order_payment"])
    else:
        sys.exit(f"невідома команда: {command}")
