"""Parser и upsert для PDF-документов DOM.РФ (декларации, разрешения, проектная,
отчётность, прочее).

Issue #297, sub-task 22i.

Источник данных: /сервисы/api/object/{obj_id}/documents  (per-object endpoint,
аналогичный /infrastructure и /photos — не входит в bulk kn/object list).

Ключи payload (наблюдаемые через chrome-devtools, структура DOM.РФ kn-API v1):

    [
      {
        "docTypeId":   1,              # int — тип по справочнику DOM.РФ
        "docTypeName": "Декларация",   # text
        "docNum":      "66-001686-Д",  # text или null
        "postedDate":  "2024-03-15",   # date string "YYYY-MM-DD" или null
        "fileUrl":     "https://xn--80az8a.../api/ext/file/...",
        "fileSize":    1234567,        # bytes, может быть null
      },
      ...
    ]

Поле doc_type нормализуется к одному из канонических значений:
    декларация / разрешение / проектная / отчётность / прочее

NOTE: download PDF-файлов — отдельная Celery-задача (future PR).
      Этот модуль только парсит и upsert-ит метаданные в domrf_kn_documents.
"""

from __future__ import annotations

import logging
from datetime import date, datetime
from typing import Any

from sqlalchemy import text
from sqlalchemy.orm import Session

logger = logging.getLogger(__name__)

# ── Canonical doc_type mapping ────────────────────────────────────────────────

_DOC_TYPE_MAP: dict[str, str] = {
    # Exact substrings → canonical type (case-insensitive, checked in order).
    "деклар": "декларация",
    "разреш": "разрешение",
    "проект": "проектная",
    "отчёт": "отчётность",
    "отчет": "отчётность",  # without ё
}
_DOC_TYPE_FALLBACK = "прочее"

_UPSERT_DOC_SQL = text(
    """
    INSERT INTO domrf_kn_documents
        (obj_id, doc_type, doc_num, posted_at, file_url, size_bytes)
    VALUES
        (:obj_id, :doc_type, :doc_num, :posted_at, :file_url, :size_bytes)
    ON CONFLICT (obj_id, doc_type, doc_num, file_url) DO UPDATE
        SET size_bytes    = EXCLUDED.size_bytes,
            local_path    = COALESCE(domrf_kn_documents.local_path, EXCLUDED.local_path),
            downloaded_at = COALESCE(domrf_kn_documents.downloaded_at, EXCLUDED.downloaded_at),
            scraped_at    = NOW()
    """
)


# ── helpers ───────────────────────────────────────────────────────────────────


def _canonical_doc_type(raw_type: str | None) -> str:
    """Нормализовать docTypeName DOM.РФ к одному из канонических значений."""
    if not raw_type:
        return _DOC_TYPE_FALLBACK
    lower = raw_type.strip().lower()
    for substr, canonical in _DOC_TYPE_MAP.items():
        if substr in lower:
            return canonical
    return _DOC_TYPE_FALLBACK


def _parse_date(v: Any) -> date | None:
    """Coerce date-like value to date. Accepts 'YYYY-MM-DD' or datetime."""
    if v is None or v == "":
        return None
    if isinstance(v, date) and not isinstance(v, datetime):
        return v
    if isinstance(v, datetime):
        return v.date()
    if isinstance(v, str):
        s = v.strip()
        for fmt in ("%Y-%m-%d", "%d.%m.%Y", "%d-%m-%Y"):
            try:
                return datetime.strptime(s, fmt).date()
            except ValueError:
                pass
    return None


# ── Public API ────────────────────────────────────────────────────────────────


def extract_documents(raw_payload: list[dict[str, Any]]) -> list[dict[str, Any]]:
    """Извлечь список документов из payload endpoint /object/{obj_id}/documents.

    Возвращает список dict, готовых для передачи в upsert_documents:
        obj_id      — передаётся отдельно в upsert_documents, здесь NOT SET
        doc_type    — канонический тип (декларация/разрешение/…)
        doc_num     — номер документа или None
        posted_at   — дата публикации или None
        file_url    — URL PDF (обязательно, строки без URL пропускаются)
        size_bytes  — размер файла в байтах или None

    Записи без file_url пропускаются с warning.
    """
    result: list[dict[str, Any]] = []
    for item in raw_payload:
        if not isinstance(item, dict):
            continue
        file_url: str | None = item.get("fileUrl") or item.get("file_url") or None
        if not file_url or not isinstance(file_url, str) or not file_url.startswith("http"):
            logger.warning("domrf documents: skipping item without valid fileUrl: %s", item)
            continue
        raw_type = item.get("docTypeName") or item.get("doc_type_name") or None
        doc_num_raw = item.get("docNum") or item.get("doc_num") or None
        doc_num = str(doc_num_raw).strip() if doc_num_raw is not None else None
        size_raw = item.get("fileSize") or item.get("file_size") or None
        try:
            size_bytes: int | None = int(size_raw) if size_raw is not None else None
        except (TypeError, ValueError):
            size_bytes = None
        result.append(
            {
                "doc_type": _canonical_doc_type(raw_type),
                "doc_num": doc_num or None,
                "posted_at": _parse_date(item.get("postedDate") or item.get("posted_date")),
                "file_url": file_url.strip(),
                "size_bytes": size_bytes,
            }
        )
    return result


def upsert_documents(db: Session, obj_id: int, docs: list[dict[str, Any]]) -> tuple[int, int]:
    """INSERT/UPDATE документов объекта в domrf_kn_documents.

    Использует SAVEPOINT (begin_nested) per-row чтобы один битый URL
    не откатывал всю транзакцию.

    Возвращает (inserted_or_updated, skipped).
    """
    if not docs:
        return 0, 0
    ok = 0
    skip = 0
    for doc in docs:
        params: dict[str, Any] = {
            "obj_id": obj_id,
            "doc_type": doc["doc_type"],
            "doc_num": doc.get("doc_num"),
            "posted_at": doc.get("posted_at"),
            "file_url": doc["file_url"],
            "size_bytes": doc.get("size_bytes"),
        }
        try:
            with db.begin_nested():
                db.execute(_UPSERT_DOC_SQL, params)
            ok += 1
        except Exception as exc:
            logger.warning(
                "upsert document obj=%s url=%s failed: %s",
                obj_id,
                doc.get("file_url"),
                exc,
            )
            skip += 1
    db.commit()
    logger.info("domrf documents obj=%s: upserted=%d skipped=%d", obj_id, ok, skip)
    return ok, skip


# ── Stub for future Celery download task ──────────────────────────────────────


def download_document_stub(obj_id: int, doc_id: int, file_url: str) -> None:
    """Placeholder для будущей Celery-задачи download_domrf_documents.

    Скачивает PDF, сохраняет в data/raw/domrf_docs/{obj_id}/{filename},
    обновляет domrf_kn_documents.local_path + downloaded_at.

    Реализация — отдельный PR (future task).
    """
    raise NotImplementedError("PDF download not implemented in 22i. See issue #297 future PR.")
