from __future__ import annotations

from datetime import datetime, timezone
from uuid import UUID

from fastapi import APIRouter, Depends, HTTPException, Request as FastapiRequest
from sqlalchemy import func
from sqlalchemy.orm import Session

from app.core.deps import require_role
from app.db.session import get_db
from app.models.admin_user import AdminUser
from app.models.attachment import Attachment
from app.models.message import Message
from app.models.request import Request
from app.models.request_data_requirement import RequestDataRequirement
from app.models.request_data_template import RequestDataTemplate
from app.models.request_data_template_item import RequestDataTemplateItem
from app.models.topic_data_template import TopicDataTemplate
from app.services.notifications import EVENT_REQUEST_DATA as NOTIFICATION_EVENT_REQUEST_DATA, notify_request_event, unread_admin_summary
from app.services.request_read_markers import EVENT_REQUEST_DATA, mark_unread_for_client
from app.services.chat_secure_service import (
    clamp_chat_window_limit,
    DEFAULT_CHAT_WINDOW_LIMIT,
    create_admin_or_lawyer_message,
    get_chat_activity_summary,
    list_messages_for_request_window,
    list_messages_for_request,
    mark_messages_delivered_for_staff,
    mark_messages_read_for_staff,
    serialize_message_for_request,
    serialize_message_bodies_for_request,
    serialize_messages_for_request,
)
from app.services.chat_presence import list_typing_presence, set_typing_presence
from app.services.security_audit import extract_client_ip, record_pii_access_event

router = APIRouter()
ALLOWED_VALUE_TYPES = {"string", "text", "date", "number", "file"}


def _audit_admin_chat_read(
    db: Session,
    *,
    admin: dict,
    http_request: FastapiRequest,
    req: Request,
    action: str,
    details: dict | None = None,
) -> None:
    record_pii_access_event(
        db,
        actor_role=str(admin.get("role") or "ADMIN").upper(),
        actor_subject=str(admin.get("sub") or admin.get("email") or ""),
        actor_ip=extract_client_ip(http_request),
        action=action,
        scope="CHAT",
        request_id=req.id,
        details=details or {},
        responsible=str(admin.get("email") or "").strip() or "Администратор системы",
        persist_now=True,
    )


def _parse_cursor(raw: str | None) -> datetime | None:
    value = str(raw or "").strip()
    if not value:
        return None
    normalized = value.replace("Z", "+00:00")
    try:
        dt = datetime.fromisoformat(normalized)
    except ValueError:
        return None
    if dt.tzinfo is None:
        return dt.replace(tzinfo=timezone.utc)
    return dt.astimezone(timezone.utc)


def _iso_or_none(dt: datetime | None) -> str | None:
    if dt is None:
        return None
    if dt.tzinfo is None:
        dt = dt.replace(tzinfo=timezone.utc)
    else:
        dt = dt.astimezone(timezone.utc)
    return dt.isoformat()


def _as_utc_datetime(value) -> datetime | None:
    if value is None:
        return None
    if isinstance(value, datetime):
        if value.tzinfo is None:
            return value.replace(tzinfo=timezone.utc)
        return value.astimezone(timezone.utc)
    if isinstance(value, str):
        return _parse_cursor(value)
    return None


def _request_uuid_or_400(request_id: str) -> UUID:
    try:
        return UUID(str(request_id))
    except ValueError:
        raise HTTPException(status_code=400, detail="Некорректный идентификатор заявки")


def _request_for_id_or_404(db: Session, request_id: str) -> Request:
    req = db.get(Request, _request_uuid_or_400(request_id))
    if req is None:
        raise HTTPException(status_code=404, detail="Заявка не найдена")
    return req


def _ensure_lawyer_can_view_request_or_403(admin: dict, req: Request) -> None:
    role = str(admin.get("role") or "").upper()
    if role != "LAWYER":
        return
    actor = str(admin.get("sub") or "").strip()
    if not actor:
        raise HTTPException(status_code=401, detail="Некорректный токен")
    assigned = str(req.assigned_lawyer_id or "").strip()
    if assigned and actor != assigned:
        raise HTTPException(status_code=403, detail="Юрист может видеть только свои и неназначенные заявки")


def _ensure_lawyer_can_manage_request_or_403(admin: dict, req: Request) -> None:
    role = str(admin.get("role") or "").upper()
    if role != "LAWYER":
        return
    actor = str(admin.get("sub") or "").strip()
    if not actor:
        raise HTTPException(status_code=401, detail="Некорректный токен")
    assigned = str(req.assigned_lawyer_id or "").strip()
    if not assigned or actor != assigned:
        raise HTTPException(status_code=403, detail="Юрист может работать только со своими назначенными заявками")


def _parse_uuid_or_400(raw: str, field_name: str) -> UUID:
    try:
        return UUID(str(raw))
    except ValueError:
        raise HTTPException(status_code=400, detail=f'Некорректное поле "{field_name}"')


def _normalize_message_ids(raw: object, *, field_name: str = "ids", limit: int = 200) -> list[UUID]:
    if not isinstance(raw, list):
        raise HTTPException(status_code=400, detail=f'Поле "{field_name}" должно быть списком')
    seen: set[UUID] = set()
    out: list[UUID] = []
    for item in raw:
        value = _parse_uuid_or_400(str(item or ""), field_name)
        if value in seen:
            continue
        seen.add(value)
        out.append(value)
        if len(out) >= limit:
            break
    if not out:
        raise HTTPException(status_code=400, detail="Нужно передать хотя бы один идентификатор сообщения")
    return out


def _slugify_key(raw: str) -> str:
    text = str(raw or "").strip().lower()
    out = []
    dash = False
    for ch in text:
        if ch.isalnum():
            out.append(ch)
            dash = False
            continue
        if not dash:
            out.append("-")
            dash = True
    slug = "".join(out).strip("-")
    return (slug or "data-field")[:80]


def _serialize_live_attachment(row: Attachment) -> dict:
    return {
        "id": str(row.id),
        "request_id": str(row.request_id),
        "message_id": str(row.message_id) if row.message_id else None,
        "file_name": row.file_name,
        "mime_type": row.mime_type,
        "size_bytes": int(row.size_bytes or 0),
        "s3_key": row.s3_key,
        "immutable": bool(row.immutable),
        "created_at": _iso_or_none(row.created_at),
        "updated_at": _iso_or_none(row.updated_at),
    }


def _normalize_value_type(raw: str | None) -> str:
    value = str(raw or "text").strip().lower()
    if value not in ALLOWED_VALUE_TYPES:
        raise HTTPException(status_code=400, detail='Тип поля должен быть одним из: string, text, date, number, file')
    return value


def _serialize_template(row: TopicDataTemplate) -> dict:
    return {
        "id": str(row.id),
        "topic_code": row.topic_code,
        "key": row.key,
        "label": row.label,
        "value_type": str(row.value_type or "text"),
        "document_name": row.document_name,
        "description": row.description,
        "sort_order": int(row.sort_order or 0),
        "enabled": bool(row.enabled),
    }


def _serialize_request_data_template(row: RequestDataTemplate) -> dict:
    return {
        "id": str(row.id),
        "topic_code": row.topic_code,
        "name": row.name,
        "description": row.description,
        "enabled": bool(row.enabled),
        "sort_order": int(row.sort_order or 0),
        "created_by_admin_id": str(row.created_by_admin_id) if row.created_by_admin_id else None,
    }


def _serialize_request_data_template_item(row: RequestDataTemplateItem) -> dict:
    return {
        "id": str(row.id),
        "request_data_template_id": str(row.request_data_template_id),
        "topic_data_template_id": str(row.topic_data_template_id) if row.topic_data_template_id else None,
        "key": row.key,
        "label": row.label,
        "value_type": str(row.value_type or "string"),
        "sort_order": int(row.sort_order or 0),
    }


def _serialize_data_request_items(db: Session, rows: list[RequestDataRequirement]) -> list[dict]:
    attachment_ids: list[UUID] = []
    for row in rows:
        if str(row.field_type or "").lower() != "file":
            continue
        try:
            attachment_ids.append(UUID(str(row.value_text or "").strip()))
        except Exception:
            continue
    attachment_map = {}
    if attachment_ids:
        attachment_rows = db.query(Attachment).filter(Attachment.id.in_(attachment_ids)).all()  # type: ignore[name-defined]
        attachment_map = {str(item.id): item for item in attachment_rows}
    out = []
    for index, row in enumerate(rows, start=1):
        value_text = str(row.value_text or "").strip()
        value_file = None
        if str(row.field_type or "").lower() == "file" and value_text:
            attachment = attachment_map.get(value_text)
            if attachment is not None:
                value_file = {
                    "attachment_id": str(attachment.id),
                    "file_name": attachment.file_name,
                    "mime_type": attachment.mime_type,
                    "size_bytes": int(attachment.size_bytes or 0),
                    "download_url": f"/api/admin/uploads/object/{attachment.id}",
                }
        out.append(
            {
                "id": str(row.id),
                "request_message_id": str(row.request_message_id) if row.request_message_id else None,
                "topic_template_id": str(row.topic_template_id) if row.topic_template_id else None,
                "key": row.key,
                "label": row.label,
                "field_type": str(row.field_type or "text"),
                "document_name": row.document_name,
                "description": row.description,
                "value_text": row.value_text,
                "value_file": value_file,
                "is_filled": bool(value_text),
                "sort_order": int(row.sort_order or 0),
                "index": index,
            }
        )
    return out


@router.get("/requests/{request_id}/messages")
def list_request_messages(
    request_id: str,
    http_request: FastapiRequest,
    db: Session = Depends(get_db),
    admin: dict = Depends(require_role("ADMIN", "LAWYER", "CURATOR")),
):
    req = _request_for_id_or_404(db, request_id)
    _ensure_lawyer_can_view_request_or_403(admin, req)
    mark_messages_read_for_staff(db, request_id=req.id)
    rows = list_messages_for_request(db, req.id)
    payload = {
        "rows": serialize_messages_for_request(
            db,
            req.id,
            rows,
            request_extra_fields=req.extra_fields,
            include_bodies=True,
        ),
        "total": len(rows),
    }
    _audit_admin_chat_read(
        db,
        admin=admin,
        http_request=http_request,
        req=req,
        action="READ_CHAT_MESSAGES",
        details={"rows": len(rows)},
    )
    return payload


@router.get("/requests/{request_id}/messages-window")
def list_request_messages_window(
    request_id: str,
    http_request: FastapiRequest,
    before_id: str | None = None,
    before_created_at: str | None = None,
    before_count: int = 0,
    limit: int = DEFAULT_CHAT_WINDOW_LIMIT,
    include_body: bool = True,
    db: Session = Depends(get_db),
    admin: dict = Depends(require_role("ADMIN", "LAWYER", "CURATOR")),
):
    req = _request_for_id_or_404(db, request_id)
    _ensure_lawyer_can_view_request_or_403(admin, req)
    mark_messages_read_for_staff(db, request_id=req.id)
    rows, has_more = list_messages_for_request_window(
        db,
        req.id,
        limit=limit,
        before_id=before_id,
        before_created_at=before_created_at,
        before_count=before_count,
    )
    message_total = int(get_chat_activity_summary(db, req.id).get("message_count") or len(rows))
    payload = {
        "rows": serialize_messages_for_request(
            db,
            req.id,
            rows,
            request_extra_fields=req.extra_fields,
            include_bodies=bool(include_body),
        ),
        "has_more": has_more,
        "total": message_total,
        "limit": clamp_chat_window_limit(limit),
    }
    _audit_admin_chat_read(
        db,
        admin=admin,
        http_request=http_request,
        req=req,
        action="READ_CHAT_MESSAGES",
        details={"rows": len(rows), "window": True},
    )
    return payload


@router.post("/requests/{request_id}/message-bodies")
def load_request_message_bodies(
    request_id: str,
    payload: dict,
    http_request: FastapiRequest,
    db: Session = Depends(get_db),
    admin: dict = Depends(require_role("ADMIN", "LAWYER", "CURATOR")),
):
    req = _request_for_id_or_404(db, request_id)
    _ensure_lawyer_can_view_request_or_403(admin, req)
    message_ids = _normalize_message_ids((payload or {}).get("ids"))
    rows = (
        db.query(Message)
        .filter(Message.request_id == req.id, Message.id.in_(message_ids))
        .order_by(Message.created_at.asc(), Message.id.asc())
        .all()
    )
    rows_by_id = {str(row.id): row for row in rows}
    ordered_rows = [rows_by_id[str(message_id)] for message_id in message_ids if str(message_id) in rows_by_id]
    result = {
        "rows": serialize_message_bodies_for_request(
            db,
            req.id,
            ordered_rows,
            request_extra_fields=req.extra_fields,
        )
    }
    _audit_admin_chat_read(
        db,
        admin=admin,
        http_request=http_request,
        req=req,
        action="READ_CHAT_MESSAGES",
        details={"rows": len(ordered_rows), "body_batch": True},
    )
    return result


@router.post("/requests/{request_id}/messages", status_code=201)
def create_request_message(
    request_id: str,
    payload: dict,
    db: Session = Depends(get_db),
    admin: dict = Depends(require_role("ADMIN", "LAWYER", "CURATOR")),
):
    req = _request_for_id_or_404(db, request_id)
    _ensure_lawyer_can_manage_request_or_403(admin, req)
    body = str((payload or {}).get("body") or "").strip()
    role = str(admin.get("role") or "").upper()
    actor_name = str(admin.get("email") or "").strip() or ("Юрист" if role == "LAWYER" else "Администратор")
    actor_admin_user_id = str(admin.get("sub") or "").strip() or None
    if actor_admin_user_id:
        try:
            actor_uuid = UUID(actor_admin_user_id)
        except ValueError:
            actor_uuid = None
        if actor_uuid is not None:
            actor_user = db.get(AdminUser, actor_uuid)
            if actor_user is not None:
                actor_name = str(actor_user.name or actor_user.email or actor_name)
    row = create_admin_or_lawyer_message(
        db,
        request=req,
        body=body,
        actor_role=role,
        actor_name=actor_name,
        actor_admin_user_id=actor_admin_user_id,
    )
    return serialize_message_for_request(row, request_extra_fields=req.extra_fields)


@router.get("/requests/{request_id}/live")
def get_request_live_state(
    request_id: str,
    http_request: FastapiRequest,
    cursor: str | None = None,
    db: Session = Depends(get_db),
    admin: dict = Depends(require_role("ADMIN", "LAWYER", "CURATOR")),
):
    req = _request_for_id_or_404(db, request_id)
    _ensure_lawyer_can_view_request_or_403(admin, req)
    mark_messages_delivered_for_staff(db, request_id=req.id)
    summary = get_chat_activity_summary(db, req.id)
    latest_activity_at = _as_utc_datetime(summary.get("latest_activity_at"))
    latest_activity_iso = _iso_or_none(latest_activity_at)
    cursor_dt = _parse_cursor(cursor)
    has_updates = bool(latest_activity_at and (cursor_dt is None or latest_activity_at > cursor_dt))
    delta_messages = []
    delta_attachments = []
    if has_updates and cursor_dt is not None:
        message_rows = (
            db.query(Message)
            .filter(
                Message.request_id == req.id,
                func.coalesce(Message.updated_at, Message.created_at) > cursor_dt,
            )
            .order_by(Message.created_at.asc(), Message.id.asc())
            .all()
        )
        attachment_rows = (
            db.query(Attachment)
            .filter(
                Attachment.request_id == req.id,
                func.coalesce(Attachment.updated_at, Attachment.created_at) > cursor_dt,
            )
            .order_by(Attachment.created_at.asc(), Attachment.id.asc())
            .all()
        )
        delta_messages = serialize_messages_for_request(
            db,
            req.id,
            message_rows,
            request_extra_fields=req.extra_fields,
            include_bodies=True,
        )
        delta_attachments = [_serialize_live_attachment(row) for row in attachment_rows]

    actor_sub = str(admin.get("sub") or "").strip() or "unknown"
    actor_role = str(admin.get("role") or "").strip().upper() or "UNKNOWN"
    actor_key = f"{actor_role}:{actor_sub}"
    typing_rows = list_typing_presence(request_key=str(req.id), exclude_actor_key=actor_key)
    payload = {
        "request_id": str(req.id),
        "cursor": latest_activity_iso,
        "has_updates": has_updates,
        "message_count": int(summary.get("message_count") or 0),
        "attachment_count": int(summary.get("attachment_count") or 0),
        "latest_message_at": _iso_or_none(_as_utc_datetime(summary.get("latest_message_at"))),
        "latest_attachment_at": _iso_or_none(_as_utc_datetime(summary.get("latest_attachment_at"))),
        "messages": delta_messages,
        "attachments": delta_attachments,
        "typing": typing_rows,
        "unread": unread_admin_summary(
            db,
            admin_user_id=str(admin.get("sub") or ""),
            request_id=req.id,
        ),
    }
    _audit_admin_chat_read(
        db,
        admin=admin,
        http_request=http_request,
        req=req,
        action="READ_CHAT_LIVE_STATE",
        details={"has_updates": bool(has_updates)},
    )
    return payload


@router.post("/requests/{request_id}/typing")
def set_request_typing_state(
    request_id: str,
    payload: dict,
    db: Session = Depends(get_db),
    admin: dict = Depends(require_role("ADMIN", "LAWYER", "CURATOR")),
):
    req = _request_for_id_or_404(db, request_id)
    _ensure_lawyer_can_manage_request_or_403(admin, req)
    actor_role = str(admin.get("role") or "").strip().upper() or "UNKNOWN"
    actor_sub = str(admin.get("sub") or "").strip() or "unknown"
    actor_email = str(admin.get("email") or "").strip()
    actor_key = f"{actor_role}:{actor_sub}"
    actor_label = actor_email or ("Юрист" if actor_role == "LAWYER" else "Администратор")
    typing = bool((payload or {}).get("typing"))
    set_typing_presence(
        request_key=str(req.id),
        actor_key=actor_key,
        actor_label=actor_label,
        actor_role=actor_role,
        typing=typing,
    )
    return {"status": "ok", "typing": typing}


@router.get("/requests/{request_id}/data-request-templates")
def list_data_request_templates(
    request_id: str,
    document: str | None = None,
    db: Session = Depends(get_db),
    admin: dict = Depends(require_role("ADMIN", "LAWYER", "CURATOR")),
):
    req = _request_for_id_or_404(db, request_id)
    _ensure_lawyer_can_manage_request_or_403(admin, req)
    topic_code = str(req.topic_code or "").strip()
    if not topic_code:
        return {"rows": [], "documents": [], "templates": []}
    query = db.query(TopicDataTemplate).filter(TopicDataTemplate.topic_code == topic_code)
    document_name = str(document or "").strip()
    if document_name:
        query = query.filter(TopicDataTemplate.document_name == document_name)
    rows = (
        query.order_by(
            TopicDataTemplate.document_name.asc().nullsfirst(),
            TopicDataTemplate.sort_order.asc(),
            TopicDataTemplate.label.asc(),
            TopicDataTemplate.key.asc(),
        ).all()
    )
    all_docs_rows = (
        db.query(TopicDataTemplate.document_name)
        .filter(TopicDataTemplate.topic_code == topic_code, TopicDataTemplate.enabled.is_(True))
        .distinct()
        .all()
    )
    documents = sorted({str(row[0]).strip() for row in all_docs_rows if row[0]}, key=lambda x: x.lower())
    template_rows = (
        db.query(RequestDataTemplate)
        .filter(RequestDataTemplate.topic_code == topic_code, RequestDataTemplate.enabled.is_(True))
        .order_by(RequestDataTemplate.sort_order.asc(), RequestDataTemplate.name.asc())
        .all()
    )
    return {
        "rows": [_serialize_template(row) for row in rows if row.enabled],  # legacy catalog payload
        "documents": documents,  # legacy
        "templates": [_serialize_request_data_template(row) for row in template_rows],
    }


@router.get("/requests/{request_id}/data-requests/{message_id}")
def get_data_request_batch(
    request_id: str,
    message_id: str,
    http_request: FastapiRequest,
    db: Session = Depends(get_db),
    admin: dict = Depends(require_role("ADMIN", "LAWYER", "CURATOR")),
):
    req = _request_for_id_or_404(db, request_id)
    _ensure_lawyer_can_view_request_or_403(admin, req)
    msg_uuid = _parse_uuid_or_400(message_id, "message_id")
    message = db.get(Message, msg_uuid)
    if message is None or message.request_id != req.id:
        raise HTTPException(status_code=404, detail="Сообщение запроса не найдено")
    rows = (
        db.query(RequestDataRequirement)
        .filter(
            RequestDataRequirement.request_id == req.id,
            RequestDataRequirement.request_message_id == msg_uuid,
        )
        .order_by(RequestDataRequirement.sort_order.asc(), RequestDataRequirement.created_at.asc(), RequestDataRequirement.id.asc())
        .all()
    )
    if not rows:
        raise HTTPException(status_code=404, detail="Набор данных для сообщения не найден")
    payload = {
        "message_id": str(message.id),
        "request_id": str(req.id),
        "track_number": req.track_number,
        "document_name": rows[0].document_name if rows else None,
        "items": _serialize_data_request_items(db, rows),
    }
    _audit_admin_chat_read(
        db,
        admin=admin,
        http_request=http_request,
        req=req,
        action="READ_CHAT_DATA_REQUEST",
        details={"message_id": str(message.id), "rows": len(rows)},
    )
    return payload


@router.get("/requests/{request_id}/data-request-templates/{template_id}")
def get_data_request_template(
    request_id: str,
    template_id: str,
    db: Session = Depends(get_db),
    admin: dict = Depends(require_role("ADMIN", "LAWYER", "CURATOR")),
):
    req = _request_for_id_or_404(db, request_id)
    _ensure_lawyer_can_manage_request_or_403(admin, req)
    template_uuid = _parse_uuid_or_400(template_id, "template_id")
    template = db.get(RequestDataTemplate, template_uuid)
    if template is None:
        raise HTTPException(status_code=404, detail="Шаблон не найден")
    if str(template.topic_code or "").strip() != str(req.topic_code or "").strip():
        raise HTTPException(status_code=400, detail="Шаблон не соответствует теме заявки")
    rows = (
        db.query(RequestDataTemplateItem)
        .filter(RequestDataTemplateItem.request_data_template_id == template.id)
        .order_by(RequestDataTemplateItem.sort_order.asc(), RequestDataTemplateItem.created_at.asc(), RequestDataTemplateItem.id.asc())
        .all()
    )
    return {
        "template": _serialize_request_data_template(template),
        "items": [_serialize_request_data_template_item(row) for row in rows],
    }


@router.post("/requests/{request_id}/data-request-templates", status_code=201)
def save_data_request_template(
    request_id: str,
    payload: dict,
    db: Session = Depends(get_db),
    admin: dict = Depends(require_role("ADMIN", "LAWYER", "CURATOR")),
):
    req = _request_for_id_or_404(db, request_id)
    _ensure_lawyer_can_manage_request_or_403(admin, req)
    topic_code = str(req.topic_code or "").strip()
    if not topic_code:
        raise HTTPException(status_code=400, detail="У заявки не указана тема")

    body = payload or {}
    name = str(body.get("name") or "").strip()
    if not name:
        raise HTTPException(status_code=400, detail="Укажите название шаблона")
    raw_items = body.get("items")
    if not isinstance(raw_items, list) or not raw_items:
        raise HTTPException(status_code=400, detail="Шаблон должен содержать хотя бы одно поле")

    actor_uuid = None
    raw_actor = str(admin.get("sub") or "").strip()
    if raw_actor:
        try:
            actor_uuid = UUID(raw_actor)
        except ValueError:
            actor_uuid = None
    responsible = str(admin.get("email") or "").strip() or "Администратор системы"

    template = None
    template_id_raw = str(body.get("template_id") or "").strip()
    if template_id_raw:
        template = db.get(RequestDataTemplate, _parse_uuid_or_400(template_id_raw, "template_id"))
        if template is None:
            raise HTTPException(status_code=404, detail="Шаблон не найден")
        if str(template.topic_code or "").strip() != topic_code:
            raise HTTPException(status_code=400, detail="Шаблон не соответствует теме заявки")
    else:
        template = (
            db.query(RequestDataTemplate)
            .filter(RequestDataTemplate.topic_code == topic_code, RequestDataTemplate.name == name)
            .first()
        )

    if template is None:
        template = RequestDataTemplate(
            topic_code=topic_code,
            name=name,
            enabled=True,
            sort_order=0,
            created_by_admin_id=actor_uuid,
            responsible=responsible,
        )
        db.add(template)
        db.flush()
    else:
        actor_role = str(admin.get("role") or "").upper()
        if actor_role == "LAWYER":
            owner_id = str(template.created_by_admin_id or "").strip()
            actor_id = str(actor_uuid or "").strip()
            if owner_id and actor_id and owner_id != actor_id:
                raise HTTPException(status_code=403, detail="Юрист может изменять только свои шаблоны")
        template.name = name
        template.responsible = responsible
        db.add(template)
        db.flush()

    touched_keys: set[str] = set()
    normalized_items: list[tuple[int, TopicDataTemplate | None, str, str, str]] = []
    for index, item in enumerate(raw_items):
        if not isinstance(item, dict):
            raise HTTPException(status_code=400, detail="Элемент шаблона должен быть объектом")
        catalog = None
        catalog_id_raw = str(item.get("topic_data_template_id") or item.get("topic_template_id") or "").strip()
        if catalog_id_raw:
            catalog = db.get(TopicDataTemplate, _parse_uuid_or_400(catalog_id_raw, "topic_data_template_id"))
            if catalog is None:
                raise HTTPException(status_code=400, detail="Поле справочника не найдено")
        label = str(item.get("label") or (catalog.label if catalog else "")).strip()
        if not label:
            raise HTTPException(status_code=400, detail="Укажите наименование поля")
        value_type = _normalize_value_type(item.get("value_type") or item.get("field_type") or (catalog.value_type if catalog else None))
        key = str(item.get("key") or (catalog.key if catalog else "")).strip()
        if not key:
            key = _slugify_key(label)
        key = key[:80]
        if key in touched_keys:
            raise HTTPException(status_code=400, detail=f'Поле "{label}" добавлено дважды')
        touched_keys.add(key)

        if catalog is None:
            catalog = (
                db.query(TopicDataTemplate)
                .filter(TopicDataTemplate.topic_code == topic_code, TopicDataTemplate.key == key)
                .first()
            )
            if catalog is None:
                catalog = TopicDataTemplate(
                    topic_code=topic_code,
                    key=key,
                    label=label,
                    value_type=value_type,
                    enabled=True,
                    required=True,
                    sort_order=index,
                    responsible=responsible,
                )
                db.add(catalog)
                db.flush()
            else:
                changed = False
                if str(catalog.label or "") != label:
                    catalog.label = label
                    changed = True
                if str(catalog.value_type or "string") != value_type:
                    catalog.value_type = value_type
                    changed = True
                if changed:
                    catalog.responsible = responsible
                    db.add(catalog)
                    db.flush()
        normalized_items.append((index, catalog, key, label, value_type))

    existing_items = (
        db.query(RequestDataTemplateItem)
        .filter(RequestDataTemplateItem.request_data_template_id == template.id)
        .all()
    )
    by_key = {str(row.key): row for row in existing_items}
    for index, catalog, key, label, value_type in normalized_items:
        row = by_key.get(key)
        if row is None:
            row = RequestDataTemplateItem(
                request_data_template_id=template.id,
                topic_data_template_id=catalog.id if catalog else None,
                key=key,
                label=label,
                value_type=value_type,
                sort_order=index,
                responsible=responsible,
            )
        else:
            row.topic_data_template_id = catalog.id if catalog else None
            row.label = label
            row.value_type = value_type
            row.sort_order = index
            row.responsible = responsible
        db.add(row)
    for row in existing_items:
        if str(row.key) not in touched_keys:
            db.delete(row)

    db.commit()
    db.refresh(template)
    items = (
        db.query(RequestDataTemplateItem)
        .filter(RequestDataTemplateItem.request_data_template_id == template.id)
        .order_by(RequestDataTemplateItem.sort_order.asc(), RequestDataTemplateItem.created_at.asc(), RequestDataTemplateItem.id.asc())
        .all()
    )
    return {"template": _serialize_request_data_template(template), "items": [_serialize_request_data_template_item(row) for row in items]}


@router.post("/requests/{request_id}/data-requests", status_code=201)
def upsert_data_request_batch(
    request_id: str,
    payload: dict,
    db: Session = Depends(get_db),
    admin: dict = Depends(require_role("ADMIN", "LAWYER", "CURATOR")),
):
    req = _request_for_id_or_404(db, request_id)
    _ensure_lawyer_can_manage_request_or_403(admin, req)
    actor_role = str(admin.get("role") or "").strip().upper()
    responsible = str(admin.get("email") or "").strip() or "Администратор системы"

    body = payload or {}
    raw_items = body.get("items")
    if not isinstance(raw_items, list) or not raw_items:
        raise HTTPException(status_code=400, detail="Нужно передать список полей запроса")

    message_id_raw = str(body.get("message_id") or "").strip()
    existing_message = None
    existing_message_rows: list[RequestDataRequirement] = []
    if message_id_raw:
        msg_uuid = _parse_uuid_or_400(message_id_raw, "message_id")
        existing_message = db.get(Message, msg_uuid)
        if existing_message is None or existing_message.request_id != req.id:
            raise HTTPException(status_code=404, detail="Сообщение запроса не найдено")
        existing_message_rows = (
            db.query(RequestDataRequirement)
            .filter(
                RequestDataRequirement.request_id == req.id,
                RequestDataRequirement.request_message_id == existing_message.id,
            )
            .all()
        )
    else:
        role = actor_role
        actor_name = str(admin.get("email") or "").strip() or ("Юрист" if role == "LAWYER" else "Администратор")
        actor_admin_user_id = str(admin.get("sub") or "").strip() or None
        if actor_admin_user_id:
            try:
                actor_uuid = UUID(actor_admin_user_id)
            except ValueError:
                actor_uuid = None
            if actor_uuid is not None:
                actor_user = db.get(AdminUser, actor_uuid)
                if actor_user is not None:
                    actor_name = str(actor_user.name or actor_user.email or actor_name)
        existing_message = create_admin_or_lawyer_message(
            db,
            request=req,
            body="Запрос",
            actor_role=role,
            actor_name=actor_name,
            actor_admin_user_id=actor_admin_user_id,
            event_type=NOTIFICATION_EVENT_REQUEST_DATA,
        )

    message_uuid = existing_message.id
    topic_code = str(req.topic_code or "").strip()
    document_name_default = str(body.get("document_name") or "").strip() or None
    actor_uuid = None
    raw_actor = str(admin.get("sub") or "").strip()
    if raw_actor:
        try:
            actor_uuid = UUID(raw_actor)
        except ValueError:
            actor_uuid = None

    normalized_rows: list[RequestDataRequirement] = []
    touched_keys: set[str] = set()
    for index, item in enumerate(raw_items):
        if not isinstance(item, dict):
            raise HTTPException(status_code=400, detail="Элемент списка полей должен быть объектом")
        template = None
        template_id_raw = str(item.get("topic_template_id") or "").strip()
        if template_id_raw:
            template = db.get(TopicDataTemplate, _parse_uuid_or_400(template_id_raw, "topic_template_id"))
            if template is None:
                raise HTTPException(status_code=400, detail="Шаблон дополнительного поля не найден")

        label = str(item.get("label") or (template.label if template else "")).strip()
        if not label:
            raise HTTPException(status_code=400, detail="Укажите наименование поля")
        field_type = _normalize_value_type(item.get("field_type") or (template.value_type if template else None))
        doc_name = str(item.get("document_name") or (template.document_name if template else "") or (document_name_default or "")).strip() or None
        key = str(item.get("key") or (template.key if template else "")).strip()
        if not key:
            key = _slugify_key(label)
        key = key[:80]
        if key in touched_keys:
            raise HTTPException(status_code=400, detail=f'Поле "{label}" добавлено дважды')
        touched_keys.add(key)

        topic_template_id = None
        if template is not None:
            topic_template_id = template.id
        elif topic_code:
            existing_template = (
                db.query(TopicDataTemplate)
                .filter(TopicDataTemplate.topic_code == topic_code, TopicDataTemplate.key == key)
                .first()
            )
            if existing_template is None:
                existing_template = TopicDataTemplate(
                    topic_code=topic_code,
                    key=key,
                    label=label,
                    value_type=field_type,
                    document_name=doc_name,
                    enabled=True,
                    required=True,
                    sort_order=index,
                    responsible=str(admin.get("email") or "").strip() or "Администратор системы",
                )
                db.add(existing_template)
                db.flush()
            else:
                changed = False
                if str(existing_template.label or "") != label:
                    existing_template.label = label
                    changed = True
                if str(existing_template.value_type or "text") != field_type:
                    existing_template.value_type = field_type
                    changed = True
                if str(existing_template.document_name or "") != str(doc_name or ""):
                    existing_template.document_name = doc_name
                    changed = True
                if changed:
                    db.add(existing_template)
                    db.flush()
            topic_template_id = existing_template.id

        req_row = (
            db.query(RequestDataRequirement)
            .filter(RequestDataRequirement.request_id == req.id, RequestDataRequirement.key == key)
            .first()
        )
        if req_row is None:
            req_row = RequestDataRequirement(
                request_id=req.id,
                request_message_id=message_uuid,
                topic_template_id=topic_template_id,
                key=key,
                label=label,
                field_type=field_type,
                document_name=doc_name,
                required=True,
                sort_order=index,
                created_by_admin_id=actor_uuid,
                responsible=str(admin.get("email") or "").strip() or "Администратор системы",
            )
        else:
            if actor_role == "LAWYER" and str(req_row.value_text or "").strip():
                current_message_id = str(req_row.request_message_id or "")
                incoming_message_id = str(message_uuid or "")
                current_topic_template_id = str(req_row.topic_template_id or "")
                incoming_topic_template_id = str(topic_template_id or "")
                current_doc_name = str(req_row.document_name or "") if req_row.document_name is not None else ""
                incoming_doc_name = str(doc_name or "")
                if (
                    str(req_row.label or "") != label
                    or str(req_row.field_type or "text") != field_type
                    or current_doc_name != incoming_doc_name
                    or current_topic_template_id != incoming_topic_template_id
                    or current_message_id != incoming_message_id
                    or int(req_row.sort_order or 0) != int(index)
                ):
                    raise HTTPException(status_code=403, detail="Юрист не может изменять заполненные клиентом данные")
            req_row.request_message_id = message_uuid
            req_row.topic_template_id = topic_template_id
            req_row.label = label
            req_row.field_type = field_type
            req_row.document_name = doc_name
            req_row.sort_order = index
            req_row.responsible = str(admin.get("email") or "").strip() or "Администратор системы"
        db.add(req_row)
        normalized_rows.append(req_row)

    if message_id_raw:
        if actor_role == "LAWYER":
            for row in existing_message_rows:
                if row.key not in touched_keys and str(row.value_text or "").strip():
                    raise HTTPException(status_code=403, detail="Юрист не может удалять заполненные клиентом данные")
        for row in existing_message_rows:
            if row.key not in touched_keys:
                db.delete(row)
        if existing_message is not None:
            existing_message.updated_at = datetime.now(timezone.utc)
            db.add(existing_message)
        mark_unread_for_client(req, EVENT_REQUEST_DATA)
        req.responsible = responsible
        db.add(req)
        notify_request_event(
            db,
            request=req,
            event_type=NOTIFICATION_EVENT_REQUEST_DATA,
            actor_role=actor_role,
            actor_admin_user_id=admin.get("sub"),
            body=f"Обновлен запрос дополнительных данных ({len(normalized_rows)})",
            responsible=responsible,
        )

    db.commit()
    fresh_messages = list_messages_for_request(db, req.id)
    serialized = serialize_messages_for_request(
        db,
        req.id,
        fresh_messages,
        request_extra_fields=req.extra_fields,
        include_bodies=True,
    )
    payload_row = next((item for item in serialized if str(item.get("id")) == str(message_uuid)), None)
    if payload_row is None:
        raise HTTPException(status_code=500, detail="Не удалось сформировать сообщение запроса")
    return payload_row
