from __future__ import annotations

import logging
from time import perf_counter
from datetime import datetime, timezone
from typing import Any
from uuid import UUID, uuid4

from fastapi import HTTPException
from sqlalchemy import case, func, or_, update
from sqlalchemy.exc import DataError, IntegrityError, SQLAlchemyError
from sqlalchemy.orm import Session

from app.models.admin_user import AdminUser
from app.models.attachment import Attachment
from app.models.audit_log import AuditLog
from app.models.invoice import Invoice
from app.models.notification import Notification
from app.models.request import Request
from app.models.request_service_request import RequestServiceRequest
from app.schemas.admin import RequestAdminCreate, RequestAdminPatch
from app.services.chat_secure_service import (
    get_chat_activity_summary,
    list_messages_for_request_window,
    mark_messages_read_for_staff,
    serialize_messages_for_request,
)
from app.schemas.universal import UniversalQuery
from app.services.billing_flow import apply_billing_transition_effects
from app.services.notifications import (
    EVENT_STATUS as NOTIFICATION_EVENT_STATUS,
    mark_admin_notifications_read,
    notify_request_event,
)
from app.services.request_assignment_events import apply_assignment_change
from app.services.request_read_markers import (
    EVENT_STATUS,
    clear_unread_for_lawyer,
    mark_unread_for_client,
)
from app.services.request_deadline import initial_important_date_at
from app.services.request_finance_validation import (
    normalize_request_financial_payload_or_400,
    request_financial_data_error_or_400,
)
from app.services.request_status import apply_status_change_effects
from app.services.request_templates import validate_required_topic_fields_or_400
from app.services.status_flow import transition_allowed_for_topic
from app.services.status_transition_requirements import validate_transition_requirements_or_400
from app.services.universal_query import apply_universal_query

from .common import normalize_important_date_or_default
from .permissions import (
    REQUEST_FINANCIAL_FIELDS,
    active_lawyer_or_400,
    client_for_request_payload_or_400,
    ensure_lawyer_can_manage_request_or_403,
    ensure_lawyer_can_view_request_or_403,
    request_uuid_or_400,
)
from .status_flow import apply_request_special_filters, get_request_status_route_service, split_request_special_filters

_WORKSPACE_LOG = logging.getLogger("uvicorn.error")
INITIAL_WORKSPACE_CHAT_WINDOW_LIMIT = 20


def query_requests_service(uq: UniversalQuery, db: Session, admin: dict) -> dict[str, Any]:
    base_query = db.query(Request)
    role = str(admin.get("role") or "").upper()
    actor = str(admin.get("sub") or "").strip()
    if role == "LAWYER":
        if not actor:
            raise HTTPException(status_code=401, detail="Некорректный токен")
        base_query = base_query.filter(
            or_(
                Request.assigned_lawyer_id == actor,
                Request.assigned_lawyer_id.is_(None),
            )
        )

    regular_uq, special_filters = split_request_special_filters(uq)
    base_query = apply_request_special_filters(
        base_query,
        db=db,
        role=role,
        actor_id=actor,
        special_filters=special_filters,
    )
    q = apply_universal_query(base_query, Request, regular_uq)
    total = q.count()
    rows = q.offset(uq.page.offset).limit(uq.page.limit).all()
    row_ids = [str(row.id) for row in rows if row and row.id]

    unread_service_requests_by_request: dict[str, int] = {}
    viewer_unread_by_request: dict[str, dict[str, Any]] = {}
    if row_ids:
        unread_query = (
            db.query(RequestServiceRequest.request_id, func.count(RequestServiceRequest.id))
            .filter(RequestServiceRequest.request_id.in_(row_ids))
        )
        if role == "LAWYER":
            unread_query = unread_query.filter(
                RequestServiceRequest.type == "CURATOR_CONTACT",
                RequestServiceRequest.assigned_lawyer_id == actor,
                RequestServiceRequest.lawyer_unread.is_(True),
            )
        else:
            unread_query = unread_query.filter(RequestServiceRequest.admin_unread.is_(True))
        unread_rows = unread_query.group_by(RequestServiceRequest.request_id).all()
        unread_service_requests_by_request = {str(request_id): int(count or 0) for request_id, count in unread_rows if request_id}

        if actor:
            try:
                actor_uuid = UUID(str(actor))
            except ValueError:
                actor_uuid = None
            if actor_uuid is not None:
                try:
                    notif_rows = (
                        db.query(Notification.request_id, Notification.event_type, func.count(Notification.id))
                        .filter(
                            Notification.recipient_type == "ADMIN_USER",
                            Notification.recipient_admin_user_id == actor_uuid,
                            Notification.is_read.is_(False),
                            Notification.request_id.in_(row_ids),
                        )
                        .group_by(Notification.request_id, Notification.event_type)
                        .all()
                    )
                except SQLAlchemyError:
                    notif_rows = []
                for request_id, event_type, count in notif_rows:
                    request_key = str(request_id or "")
                    if not request_key:
                        continue
                    bucket = viewer_unread_by_request.setdefault(request_key, {"total": 0, "by_event": {}})
                    event_key = str(event_type or "").strip().upper()
                    event_count = int(count or 0)
                    if event_key:
                        bucket["by_event"][event_key] = int(bucket["by_event"].get(event_key, 0)) + event_count
                    bucket["total"] = int(bucket["total"]) + event_count

    return {
        "rows": [
            {
                "id": str(r.id),
                "track_number": r.track_number,
                "client_id": str(r.client_id) if r.client_id else None,
                "status_code": r.status_code,
                "client_name": r.client_name,
                "client_phone": r.client_phone,
                "topic_code": r.topic_code,
                "important_date_at": r.important_date_at.isoformat() if r.important_date_at else None,
                "effective_rate": float(r.effective_rate) if r.effective_rate is not None else None,
                "request_cost": float(r.request_cost) if r.request_cost is not None else None,
                "invoice_amount": float(r.invoice_amount) if r.invoice_amount is not None else None,
                "paid_at": r.paid_at.isoformat() if r.paid_at else None,
                "paid_by_admin_id": r.paid_by_admin_id,
                "client_has_unread_updates": r.client_has_unread_updates,
                "client_unread_event_type": r.client_unread_event_type,
                "lawyer_has_unread_updates": r.lawyer_has_unread_updates,
                "lawyer_unread_event_type": r.lawyer_unread_event_type,
                "service_requests_unread_count": int(unread_service_requests_by_request.get(str(r.id), 0)),
                "has_service_requests_unread": bool(unread_service_requests_by_request.get(str(r.id), 0)),
                "viewer_unread_total": int((viewer_unread_by_request.get(str(r.id)) or {}).get("total", 0)),
                "viewer_unread_by_event": dict((viewer_unread_by_request.get(str(r.id)) or {}).get("by_event", {})),
                "created_at": r.created_at.isoformat() if r.created_at else None,
                "updated_at": r.updated_at.isoformat() if r.updated_at else None,
            }
            for r in rows
        ],
        "total": total,
    }


def create_request_service(payload: RequestAdminCreate, db: Session, admin: dict) -> dict[str, Any]:
    actor_role = str(admin.get("role") or "").upper()
    if actor_role == "LAWYER" and str(payload.assigned_lawyer_id or "").strip():
        raise HTTPException(status_code=403, detail="Юрист не может назначать заявку при создании")
    if actor_role == "LAWYER":
        forbidden_fields = sorted(REQUEST_FINANCIAL_FIELDS.intersection(set(payload.model_fields_set)))
        if forbidden_fields:
            raise HTTPException(status_code=403, detail="Юрист не может изменять финансовые поля заявки")
    validate_required_topic_fields_or_400(db, payload.topic_code, payload.extra_fields)
    track = payload.track_number or f"TRK-{uuid4().hex[:10].upper()}"
    responsible = str(admin.get("email") or "").strip() or "Администратор системы"
    client = client_for_request_payload_or_400(
        db,
        client_id=payload.client_id,
        client_name=payload.client_name,
        client_phone=payload.client_phone,
        responsible=responsible,
    )
    finance_payload = normalize_request_financial_payload_or_400(payload.model_dump())
    assigned_lawyer_id = str(payload.assigned_lawyer_id or "").strip() or None
    effective_rate = finance_payload.get("effective_rate")
    if assigned_lawyer_id:
        assigned_lawyer = active_lawyer_or_400(db, assigned_lawyer_id)
        assigned_lawyer_id = str(assigned_lawyer.id)
        if effective_rate is None:
            effective_rate = assigned_lawyer.default_rate
    important_date_at = payload.important_date_at or initial_important_date_at()
    row = Request(
        track_number=track,
        client_id=client.id,
        client_name=client.full_name,
        client_phone=client.phone,
        topic_code=payload.topic_code,
        status_code=payload.status_code,
        important_date_at=important_date_at,
        description=payload.description,
        extra_fields=payload.extra_fields,
        assigned_lawyer_id=assigned_lawyer_id,
        effective_rate=effective_rate,
        request_cost=finance_payload.get("request_cost"),
        invoice_amount=finance_payload.get("invoice_amount"),
        paid_at=payload.paid_at,
        paid_by_admin_id=payload.paid_by_admin_id,
        total_attachments_bytes=payload.total_attachments_bytes,
        responsible=responsible,
    )
    try:
        db.add(row)
        db.commit()
        db.refresh(row)
    except DataError as exc:
        db.rollback()
        raise request_financial_data_error_or_400() from exc
    except IntegrityError as exc:
        db.rollback()
        raise HTTPException(status_code=400, detail="Заявка с таким номером уже существует") from exc
    if assigned_lawyer_id:
        apply_assignment_change(
            db,
            request=row,
            old_lawyer_id=None,
            new_lawyer_id=assigned_lawyer_id,
            actor_role=str(admin.get("role") or "").upper() or "ADMIN",
            actor_admin_user_id=admin.get("sub"),
            responsible=responsible,
            actor_name=str(admin.get("email") or "").strip() or "Администратор системы",
        )
        db.add(row)
        db.commit()
        db.refresh(row)
    return {"id": str(row.id), "track_number": row.track_number}


def update_request_service(request_id: str, payload: RequestAdminPatch, db: Session, admin: dict) -> dict[str, Any]:
    request_uuid = request_uuid_or_400(request_id)
    row = db.get(Request, request_uuid)
    if not row:
        raise HTTPException(status_code=404, detail="Заявка не найдена")
    ensure_lawyer_can_manage_request_or_403(admin, row)
    changes = payload.model_dump(exclude_unset=True)
    changes = normalize_request_financial_payload_or_400(changes)
    actor_role = str(admin.get("role") or "").upper()
    if actor_role == "LAWYER":
        if "assigned_lawyer_id" in changes:
            raise HTTPException(status_code=403, detail='Назначение доступно только через действие "Взять в работу"')
        forbidden_fields = sorted(REQUEST_FINANCIAL_FIELDS.intersection(set(changes.keys())))
        if forbidden_fields:
            raise HTTPException(status_code=403, detail="Юрист не может изменять финансовые поля заявки")
    if actor_role == "ADMIN" and "assigned_lawyer_id" in changes:
        assigned_raw = changes.get("assigned_lawyer_id")
        if assigned_raw is None or not str(assigned_raw).strip():
            changes["assigned_lawyer_id"] = None
        else:
            assigned_lawyer = active_lawyer_or_400(db, str(assigned_raw))
            changes["assigned_lawyer_id"] = str(assigned_lawyer.id)
            if row.effective_rate is None and "effective_rate" not in changes:
                changes["effective_rate"] = assigned_lawyer.default_rate
    old_status = str(row.status_code or "")
    old_assigned_lawyer_id = str(row.assigned_lawyer_id or "").strip()
    responsible = str(admin.get("email") or "").strip() or "Администратор системы"
    if {"client_id", "client_name", "client_phone"}.intersection(set(changes.keys())):
        client = client_for_request_payload_or_400(
            db,
            client_id=changes.get("client_id", row.client_id),
            client_name=changes.get("client_name", row.client_name),
            client_phone=changes.get("client_phone", row.client_phone),
            responsible=responsible,
        )
        changes["client_id"] = client.id
        changes["client_name"] = client.full_name
        changes["client_phone"] = client.phone
    status_changed = "status_code" in changes and str(changes.get("status_code") or "") != old_status
    if status_changed and ("important_date_at" not in changes or changes.get("important_date_at") is None):
        changes["important_date_at"] = normalize_important_date_or_default(None)
    if status_changed:
        next_status = str(changes.get("status_code") or "").strip()
        if not transition_allowed_for_topic(
            db,
            str(row.topic_code or "").strip() or None,
            old_status,
            next_status,
        ):
            raise HTTPException(status_code=400, detail="Переход статуса не разрешен для выбранной темы")
        extra_fields_override = changes.get("extra_fields")
        validate_transition_requirements_or_400(
            db,
            row,
            old_status,
            next_status,
            extra_fields_override=extra_fields_override if isinstance(extra_fields_override, dict) else None,
        )
    for key, value in changes.items():
        setattr(row, key, value)
    new_assigned_lawyer_id = str(row.assigned_lawyer_id or "").strip()
    assigned_changed = old_assigned_lawyer_id != new_assigned_lawyer_id
    if status_changed:
        next_status = str(changes.get("status_code") or "")
        important_date_at = row.important_date_at
        billing_note = apply_billing_transition_effects(
            db,
            req=row,
            from_status=old_status,
            to_status=next_status,
            admin=admin,
            responsible=responsible,
        )
        mark_unread_for_client(row, EVENT_STATUS)
        apply_status_change_effects(
            db,
            row,
            from_status=old_status,
            to_status=next_status,
            admin=admin,
            responsible=responsible,
        )
        notify_request_event(
            db,
            request=row,
            event_type=NOTIFICATION_EVENT_STATUS,
            actor_role=str(admin.get("role") or "").upper() or "ADMIN",
            actor_admin_user_id=admin.get("sub"),
            body=(
                f"{old_status} -> {next_status}"
                + (f"\nВажная дата: {important_date_at.isoformat()}" if important_date_at else "")
                + (f"\n{billing_note}" if billing_note else "")
            ),
            responsible=responsible,
        )
    if actor_role == "ADMIN" and assigned_changed and new_assigned_lawyer_id:
        apply_assignment_change(
            db,
            request=row,
            old_lawyer_id=old_assigned_lawyer_id,
            new_lawyer_id=new_assigned_lawyer_id,
            actor_role="ADMIN",
            actor_admin_user_id=admin.get("sub"),
            responsible=responsible,
            actor_name=str(admin.get("email") or "").strip() or "Администратор системы",
        )
    try:
        db.add(row)
        db.commit()
        db.refresh(row)
    except DataError as exc:
        db.rollback()
        raise request_financial_data_error_or_400() from exc
    except IntegrityError as exc:
        db.rollback()
        raise HTTPException(status_code=400, detail="Заявка с таким номером уже существует") from exc
    return {"status": "обновлено", "id": str(row.id), "track_number": row.track_number}


def delete_request_service(request_id: str, db: Session, admin: dict) -> dict[str, Any]:
    request_uuid = request_uuid_or_400(request_id)
    row = db.get(Request, request_uuid)
    if not row:
        raise HTTPException(status_code=404, detail="Заявка не найдена")
    ensure_lawyer_can_manage_request_or_403(admin, row)
    db.delete(row)
    db.commit()
    return {"status": "удалено"}


def get_request_service(request_id: str, db: Session, admin: dict) -> dict[str, Any]:
    request_uuid = request_uuid_or_400(request_id)
    req = db.get(Request, request_uuid)
    if not req:
        raise HTTPException(status_code=404, detail="Заявка не найдена")
    ensure_lawyer_can_view_request_or_403(admin, req)
    _apply_request_open_side_effects(db, req, admin, mark_chat_read=False)
    return _serialize_request_row(req)


def _serialize_request_row(req: Request) -> dict[str, Any]:
    return {
        "id": str(req.id),
        "track_number": req.track_number,
        "client_id": str(req.client_id) if req.client_id else None,
        "client_name": req.client_name,
        "client_phone": req.client_phone,
        "topic_code": req.topic_code,
        "status_code": req.status_code,
        "important_date_at": req.important_date_at.isoformat() if req.important_date_at else None,
        "description": req.description,
        "extra_fields": req.extra_fields,
        "assigned_lawyer_id": req.assigned_lawyer_id,
        "effective_rate": float(req.effective_rate) if req.effective_rate is not None else None,
        "request_cost": float(req.request_cost) if req.request_cost is not None else None,
        "invoice_amount": float(req.invoice_amount) if req.invoice_amount is not None else None,
        "paid_at": req.paid_at.isoformat() if req.paid_at else None,
        "paid_by_admin_id": req.paid_by_admin_id,
        "total_attachments_bytes": req.total_attachments_bytes,
        "client_has_unread_updates": req.client_has_unread_updates,
        "client_unread_event_type": req.client_unread_event_type,
        "lawyer_has_unread_updates": req.lawyer_has_unread_updates,
        "lawyer_unread_event_type": req.lawyer_unread_event_type,
        "created_at": req.created_at.isoformat() if req.created_at else None,
        "updated_at": req.updated_at.isoformat() if req.updated_at else None,
    }


def _apply_request_open_side_effects(db: Session, req: Request, admin: dict, *, mark_chat_read: bool) -> None:
    changed = False
    if str(admin.get("role") or "").upper() == "LAWYER" and clear_unread_for_lawyer(req):
        changed = True
        db.add(req)
    read_count = mark_admin_notifications_read(
        db,
        admin_user_id=admin.get("sub"),
        request_id=req.id,
        responsible=str(admin.get("email") or "").strip() or "Администратор системы",
    )
    if read_count:
        changed = True
    if mark_chat_read and mark_messages_read_for_staff(db, request_id=req.id, commit=False):
        changed = True
    if changed:
        db.commit()
        if str(admin.get("role") or "").upper() == "LAWYER":
            db.refresh(req)


def _serialize_request_attachment(row: Attachment) -> dict[str, Any]:
    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),
        "scan_status": row.scan_status,
        "scan_signature": row.scan_signature,
        "scan_error": row.scan_error,
        "scanned_at": row.scanned_at.isoformat() if row.scanned_at else None,
        "created_at": row.created_at.isoformat() if row.created_at else None,
        "updated_at": row.updated_at.isoformat() if row.updated_at else None,
        "responsible": row.responsible,
    }


def _serialize_request_invoice(row: Invoice) -> dict[str, Any]:
    return {
        "id": str(row.id),
        "invoice_number": row.invoice_number,
        "request_id": str(row.request_id),
        "client_id": str(row.client_id) if row.client_id else None,
        "status": row.status,
        "amount": float(row.amount) if row.amount is not None else None,
        "currency": row.currency,
        "payer_display_name": row.payer_display_name,
        "issued_by_admin_user_id": str(row.issued_by_admin_user_id) if row.issued_by_admin_user_id else None,
        "issued_by_role": row.issued_by_role,
        "issued_at": row.issued_at.isoformat() if row.issued_at else None,
        "paid_at": row.paid_at.isoformat() if row.paid_at else None,
        "created_at": row.created_at.isoformat() if row.created_at else None,
        "updated_at": row.updated_at.isoformat() if row.updated_at else None,
        "responsible": row.responsible,
        "pdf_url": f"/api/admin/invoices/{row.id}/pdf",
    }


def get_request_workspace_service(request_id: str, db: Session, admin: dict, *, include_related: bool = True) -> dict[str, Any]:
    started_at = perf_counter()
    request_uuid = request_uuid_or_400(request_id)
    req = db.get(Request, request_uuid)
    if req is None:
        raise HTTPException(status_code=404, detail="Заявка не найдена")
    ensure_lawyer_can_view_request_or_403(admin, req)

    side_effects_started_at = perf_counter()
    _apply_request_open_side_effects(db, req, admin, mark_chat_read=True)
    side_effects_ms = (perf_counter() - side_effects_started_at) * 1000.0

    serialize_request_started_at = perf_counter()
    request_payload = _serialize_request_row(req)
    request_row_ms = (perf_counter() - serialize_request_started_at) * 1000.0

    messages_started_at = perf_counter()
    message_rows, messages_has_more = list_messages_for_request_window(
        db,
        req.id,
        limit=INITIAL_WORKSPACE_CHAT_WINDOW_LIMIT,
        before_count=0,
    )
    messages_query_ms = (perf_counter() - messages_started_at) * 1000.0

    serialize_messages_started_at = perf_counter()
    serialized_messages = serialize_messages_for_request(
        db,
        req.id,
        message_rows,
        request_extra_fields=req.extra_fields,
        include_bodies=False,
    )
    serialize_messages_ms = (perf_counter() - serialize_messages_started_at) * 1000.0
    messages_total = int(get_chat_activity_summary(db, req.id).get("message_count") or len(message_rows))
    messages_loaded_count = len(message_rows)

    attachment_rows: list[Attachment] = []
    invoice_rows: list[Invoice] = []
    status_route_payload: dict[str, Any] = {
        "nodes": [],
        "history": [],
        "available_statuses": [],
        "current_important_date_at": request_payload.get("important_date_at"),
    }
    attachments_query_ms = 0.0
    invoices_query_ms = 0.0
    status_route_ms = 0.0
    if include_related:
        attachments_started_at = perf_counter()
        attachment_rows = (
            db.query(Attachment)
            .filter(Attachment.request_id == req.id)
            .order_by(Attachment.created_at.asc(), Attachment.id.asc())
            .all()
        )
        attachments_query_ms = (perf_counter() - attachments_started_at) * 1000.0
        role = str(admin.get("role") or "").upper()
        if role in {"ADMIN", "LAWYER"}:
            invoices_started_at = perf_counter()
            invoice_rows = (
                db.query(Invoice)
                .filter(Invoice.request_id == req.id)
                .order_by(Invoice.issued_at.desc(), Invoice.id.desc())
                .all()
            )
            invoices_query_ms = (perf_counter() - invoices_started_at) * 1000.0
        status_route_started_at = perf_counter()
        status_route_payload = get_request_status_route_service(request_id, db, admin, request_row=req)
        status_route_ms = (perf_counter() - status_route_started_at) * 1000.0

    paid_invoices = [row for row in invoice_rows if str(row.status or "").upper() == "PAID"]
    paid_total = round(sum(float(row.amount or 0) for row in paid_invoices), 2)
    latest_paid_at = None
    for row in paid_invoices:
        if row.paid_at is None:
            continue
        if latest_paid_at is None or row.paid_at > latest_paid_at:
            latest_paid_at = row.paid_at

    payload = {
        "request": request_payload,
        "messages": serialized_messages,
        "messages_total": messages_total,
        "messages_has_more": messages_has_more,
        "messages_loaded_count": messages_loaded_count,
        "attachments": [_serialize_request_attachment(row) for row in attachment_rows],
        "invoices": [_serialize_request_invoice(row) for row in invoice_rows],
        "finance_summary": {
            "request_cost": request_payload.get("request_cost"),
            "effective_rate": request_payload.get("effective_rate"),
            "paid_total": paid_total,
            "last_paid_at": latest_paid_at.isoformat() if latest_paid_at else request_payload.get("paid_at"),
        },
        "status_route": status_route_payload,
    }
    total_ms = (perf_counter() - started_at) * 1000.0
    _WORKSPACE_LOG.info(
        "workspace request_id=%s include_related=%s total_ms=%.2f side_effects_ms=%.2f request_row_ms=%.2f "
        "messages_query_ms=%.2f serialize_messages_ms=%.2f attachments_query_ms=%.2f invoices_query_ms=%.2f "
        "status_route_ms=%.2f messages=%s attachments=%s invoices=%s messages_total=%s",
        str(req.id),
        bool(include_related),
        total_ms,
        side_effects_ms,
        request_row_ms,
        messages_query_ms,
        serialize_messages_ms,
        attachments_query_ms,
        invoices_query_ms,
        status_route_ms,
        len(message_rows),
        len(attachment_rows),
        len(invoice_rows),
        messages_total,
    )
    return payload


def claim_request_service(request_id: str, db: Session, admin: dict) -> dict[str, Any]:
    request_uuid = request_uuid_or_400(request_id)

    lawyer_sub = str(admin.get("sub") or "").strip()
    if not lawyer_sub:
        raise HTTPException(status_code=401, detail="Некорректный токен")
    try:
        lawyer_uuid = UUID(lawyer_sub)
    except ValueError as exc:
        raise HTTPException(status_code=401, detail="Некорректный токен") from exc

    lawyer = db.get(AdminUser, lawyer_uuid)
    if not lawyer or str(lawyer.role or "").upper() != "LAWYER" or not bool(lawyer.is_active):
        raise HTTPException(status_code=403, detail="Доступно только активному юристу")

    now = datetime.now(timezone.utc)
    responsible = str(admin.get("email") or "").strip() or "Администратор системы"

    stmt = (
        update(Request)
        .where(Request.id == request_uuid, Request.assigned_lawyer_id.is_(None))
        .values(
            assigned_lawyer_id=str(lawyer_uuid),
            effective_rate=case((Request.effective_rate.is_(None), lawyer.default_rate), else_=Request.effective_rate),
            updated_at=now,
            responsible=responsible,
        )
    )

    try:
        updated_rows = db.execute(stmt).rowcount or 0
        if updated_rows == 0:
            existing = db.get(Request, request_uuid)
            if existing is None:
                db.rollback()
                raise HTTPException(status_code=404, detail="Заявка не найдена")
            db.rollback()
            raise HTTPException(status_code=409, detail="Заявка уже назначена")

        db.add(
            AuditLog(
                actor_admin_id=lawyer_uuid,
                entity="requests",
                entity_id=str(request_uuid),
                action="MANUAL_CLAIM",
                diff={"assigned_lawyer_id": str(lawyer_uuid)},
            )
        )
        db.commit()
    except HTTPException:
        raise
    except Exception:
        db.rollback()
        raise

    row = db.get(Request, request_uuid)
    if row is None:
        raise HTTPException(status_code=404, detail="Заявка не найдена")

    apply_assignment_change(
        db,
        request=row,
        old_lawyer_id=None,
        new_lawyer_id=str(lawyer_uuid),
        actor_role="LAWYER",
        actor_admin_user_id=str(lawyer_uuid),
        responsible=responsible,
        actor_name=str(lawyer.name or lawyer.email or lawyer_uuid),
    )
    db.add(row)
    db.commit()
    db.refresh(row)

    return {
        "status": "claimed",
        "id": str(row.id),
        "track_number": row.track_number,
        "assigned_lawyer_id": row.assigned_lawyer_id,
    }


def reassign_request_service(request_id: str, lawyer_id: str, db: Session, admin: dict) -> dict[str, Any]:
    request_uuid = request_uuid_or_400(request_id)

    try:
        lawyer_uuid = UUID(str(lawyer_id))
    except ValueError as exc:
        raise HTTPException(status_code=400, detail="Некорректный идентификатор юриста") from exc

    target_lawyer = db.get(AdminUser, lawyer_uuid)
    if not target_lawyer or str(target_lawyer.role or "").upper() != "LAWYER" or not bool(target_lawyer.is_active):
        raise HTTPException(status_code=400, detail="Можно переназначить только на активного юриста")

    req = db.get(Request, request_uuid)
    if req is None:
        raise HTTPException(status_code=404, detail="Заявка не найдена")
    if req.assigned_lawyer_id is None:
        raise HTTPException(status_code=400, detail="Заявка не назначена")
    if str(req.assigned_lawyer_id) == str(lawyer_uuid):
        raise HTTPException(status_code=400, detail="Заявка уже назначена на выбранного юриста")

    old_assigned = str(req.assigned_lawyer_id)
    now = datetime.now(timezone.utc)
    responsible = str(admin.get("email") or "").strip() or "Администратор системы"
    admin_actor_id = None
    try:
        admin_actor_id = UUID(str(admin.get("sub") or ""))
    except ValueError:
        admin_actor_id = None

    stmt = (
        update(Request)
        .where(Request.id == request_uuid, Request.assigned_lawyer_id == old_assigned)
        .values(
            assigned_lawyer_id=str(lawyer_uuid),
            effective_rate=case((Request.effective_rate.is_(None), target_lawyer.default_rate), else_=Request.effective_rate),
            updated_at=now,
            responsible=responsible,
        )
    )

    try:
        updated_rows = db.execute(stmt).rowcount or 0
        if updated_rows == 0:
            db.rollback()
            raise HTTPException(status_code=409, detail="Заявка уже была переназначена")

        db.add(
            AuditLog(
                actor_admin_id=admin_actor_id,
                entity="requests",
                entity_id=str(request_uuid),
                action="MANUAL_REASSIGN",
                diff={"from_lawyer_id": old_assigned, "to_lawyer_id": str(lawyer_uuid)},
            )
        )
        db.commit()
    except HTTPException:
        raise
    except Exception:
        db.rollback()
        raise

    row = db.get(Request, request_uuid)
    if row is None:
        raise HTTPException(status_code=404, detail="Заявка не найдена")

    apply_assignment_change(
        db,
        request=row,
        old_lawyer_id=old_assigned,
        new_lawyer_id=str(lawyer_uuid),
        actor_role="ADMIN",
        actor_admin_user_id=admin.get("sub"),
        responsible=responsible,
        actor_name=str(admin.get("email") or "").strip() or "Администратор системы",
    )
    db.add(row)
    db.commit()
    db.refresh(row)

    return {
        "status": "reassigned",
        "id": str(row.id),
        "track_number": row.track_number,
        "from_lawyer_id": old_assigned,
        "assigned_lawyer_id": row.assigned_lawyer_id,
    }
