from __future__ import annotations

import json
from datetime import datetime, timezone
from decimal import Decimal
from uuid import UUID

from fastapi import APIRouter, Depends, HTTPException, Request as FastapiRequest
from fastapi.responses import StreamingResponse
from sqlalchemy.exc import IntegrityError
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.invoice import Invoice
from app.models.request import Request
from app.schemas.universal import UniversalQuery
from app.services.invoice_chat import create_invoice_chat_message_with_attachment
from app.services.invoice_crypto import decrypt_requisites, encrypt_requisites
from app.services.invoice_numbering import generate_invoice_number
from app.services.invoice_pdf import build_invoice_pdf_bytes
from app.services.security_audit import extract_client_ip, record_pii_access_event
from app.services.universal_query import apply_universal_query

router = APIRouter()

STATUS_WAITING = "WAITING_PAYMENT"
STATUS_PAID = "PAID"
STATUS_CANCELED = "CANCELED"
ALLOWED_STATUSES = {STATUS_WAITING, STATUS_PAID, STATUS_CANCELED}
STATUS_LABELS = {
    STATUS_WAITING: "Ожидает оплату",
    STATUS_PAID: "Оплачен",
    STATUS_CANCELED: "Отменен",
}


def _to_float(value) -> float | None:
    if value is None:
        return None
    if isinstance(value, Decimal):
        return float(value)
    try:
        return float(value)
    except (TypeError, ValueError):
        return None


def _to_iso(value: datetime | None) -> str | None:
    return value.isoformat() if value else None


def _actor_uuid_or_401(admin: dict) -> UUID:
    try:
        return UUID(str(admin.get("sub") or ""))
    except ValueError:
        raise HTTPException(status_code=401, detail="Некорректный токен")


def _uuid_or_400(raw: str | None, field: str) -> UUID:
    if not raw:
        raise HTTPException(status_code=400, detail=f'Поле "{field}" обязательно')
    try:
        return UUID(str(raw))
    except ValueError:
        raise HTTPException(status_code=400, detail=f'Некорректное поле "{field}"')


def _normalize_status(raw: str | None) -> str:
    value = str(raw or STATUS_WAITING).strip().upper()
    if value not in ALLOWED_STATUSES:
        raise HTTPException(status_code=400, detail="Некорректный статус счета")
    return value


def _normalize_currency(raw: str | None) -> str:
    value = str(raw or "RUB").strip().upper()[:3]
    return value or "RUB"


def _amount_or_400(raw) -> float:
    value = _to_float(raw)
    if value is None:
        raise HTTPException(status_code=400, detail='Поле "amount" обязательно и должно быть числом')
    if value < 0:
        raise HTTPException(status_code=400, detail='Поле "amount" не может быть отрицательным')
    return round(value, 2)


def _now_utc() -> datetime:
    return datetime.now(timezone.utc)


def _parse_requisites(raw) -> dict:
    if raw is None:
        return {}
    if isinstance(raw, dict):
        return dict(raw)
    text = str(raw).strip()
    if not text:
        return {}
    try:
        data = json.loads(text)
    except Exception:
        raise HTTPException(status_code=400, detail='Поле "payer_details" должно быть JSON-объектом')
    if not isinstance(data, dict):
        raise HTTPException(status_code=400, detail='Поле "payer_details" должно быть JSON-объектом')
    return data


def _ensure_lawyer_owns_request_or_403(role: str, actor_id: UUID, req: Request) -> None:
    if role != "LAWYER":
        return
    assigned = str(req.assigned_lawyer_id or "").strip()
    if not assigned or assigned != str(actor_id):
        raise HTTPException(status_code=403, detail="Юрист видит и изменяет только свои счета")


def _serialize_invoice(
    row: Invoice,
    request_track: str | None,
    issuer_name: str | None,
    *,
    include_payer_details: bool = False,
) -> dict:
    payload = {
        "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,
        "request_track_number": request_track,
        "status": row.status,
        "status_label": STATUS_LABELS.get(str(row.status or "").upper(), row.status),
        "amount": _to_float(row.amount),
        "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_name": issuer_name,
        "issued_by_role": row.issued_by_role,
        "issued_at": _to_iso(row.issued_at),
        "paid_at": _to_iso(row.paid_at),
        "created_at": _to_iso(row.created_at),
        "updated_at": _to_iso(row.updated_at),
        "responsible": row.responsible,
        "pdf_url": f"/api/admin/invoices/{row.id}/pdf",
    }
    if include_payer_details:
        payload["payer_details"] = decrypt_requisites(row.payer_details_encrypted)
    return payload


def _apply_paid_flags(req: Request, invoice: Invoice, *, admin_id: UUID | None) -> None:
    req.invoice_amount = invoice.amount
    req.paid_at = invoice.paid_at
    req.paid_by_admin_id = str(admin_id) if admin_id else None


def _request_from_payload_or_404(db: Session, payload: dict) -> Request:
    request_id_raw = payload.get("request_id")
    track_number_raw = str(payload.get("request_track_number") or "").strip().upper()

    if request_id_raw:
        request_id = _uuid_or_400(request_id_raw, "request_id")
        req = db.get(Request, request_id)
        if req is None:
            raise HTTPException(status_code=404, detail="Заявка не найдена")
        return req

    if track_number_raw:
        req = db.query(Request).filter(Request.track_number == track_number_raw).first()
        if req is None:
            raise HTTPException(status_code=404, detail="Заявка не найдена")
        return req

    raise HTTPException(status_code=400, detail='Поле "request_id" или "request_track_number" обязательно')


def _commit_or_400(db: Session, detail: str) -> None:
    try:
        db.commit()
    except IntegrityError:
        db.rollback()
        raise HTTPException(status_code=400, detail=detail)


@router.post("/query")
def query_invoices(
    uq: UniversalQuery,
    http_request: FastapiRequest,
    db: Session = Depends(get_db),
    admin: dict = Depends(require_role("ADMIN", "LAWYER")),
):
    role = str(admin.get("role") or "").upper()
    actor_id = _actor_uuid_or_401(admin)

    query = db.query(Invoice)
    if role == "LAWYER":
        query = query.join(Request, Request.id == Invoice.request_id).filter(Request.assigned_lawyer_id == str(actor_id))
    query = apply_universal_query(query, Invoice, uq)

    total = query.count()
    rows = query.offset(uq.page.offset).limit(uq.page.limit).all()

    request_ids = {row.request_id for row in rows}
    requests = db.query(Request.id, Request.track_number).filter(Request.id.in_(request_ids)).all() if request_ids else []
    request_map = {str(row_id): track for row_id, track in requests}

    issuer_ids = {row.issued_by_admin_user_id for row in rows if row.issued_by_admin_user_id}
    users = db.query(AdminUser.id, AdminUser.name, AdminUser.email).filter(AdminUser.id.in_(issuer_ids)).all() if issuer_ids else []
    issuer_map = {str(user_id): (name or email or str(user_id)) for user_id, name, email in users}

    data = [
        _serialize_invoice(
            row,
            request_track=request_map.get(str(row.request_id)),
            issuer_name=issuer_map.get(str(row.issued_by_admin_user_id)) if row.issued_by_admin_user_id else None,
        )
        for row in rows
    ]
    payload = {"rows": data, "total": int(total)}
    record_pii_access_event(
        db,
        actor_role=role,
        actor_subject=str(admin.get("sub") or admin.get("email") or ""),
        actor_ip=extract_client_ip(http_request),
        action="READ_INVOICE_LIST",
        scope="INVOICE",
        details={"rows": int(total)},
        responsible=str(admin.get("email") or "").strip() or "Администратор системы",
        persist_now=True,
    )
    return payload


@router.get("/by-request/{request_id}")
def list_invoices_by_request(
    request_id: str,
    http_request: FastapiRequest,
    db: Session = Depends(get_db),
    admin: dict = Depends(require_role("ADMIN", "LAWYER")),
):
    role = str(admin.get("role") or "").upper()
    actor_id = _actor_uuid_or_401(admin)

    req = db.get(Request, _uuid_or_400(request_id, "request_id"))
    if req is None:
        raise HTTPException(status_code=404, detail="Заявка не найдена")
    _ensure_lawyer_owns_request_or_403(role, actor_id, req)

    rows = db.query(Invoice).filter(Invoice.request_id == req.id).order_by(Invoice.issued_at.desc(), Invoice.id.desc()).all()
    issuer_ids = {row.issued_by_admin_user_id for row in rows if row.issued_by_admin_user_id}
    users = db.query(AdminUser.id, AdminUser.name, AdminUser.email).filter(AdminUser.id.in_(issuer_ids)).all() if issuer_ids else []
    issuer_map = {str(user_id): (name or email or str(user_id)) for user_id, name, email in users}

    payload = {
        "rows": [
            _serialize_invoice(
                row,
                request_track=req.track_number,
                issuer_name=issuer_map.get(str(row.issued_by_admin_user_id)) if row.issued_by_admin_user_id else None,
            )
            for row in rows
        ],
        "total": len(rows),
    }
    record_pii_access_event(
        db,
        actor_role=role,
        actor_subject=str(admin.get("sub") or admin.get("email") or ""),
        actor_ip=extract_client_ip(http_request),
        action="READ_REQUEST_INVOICE_LIST",
        scope="INVOICE",
        request_id=req.id,
        details={"rows": int(len(rows)), "track_number": req.track_number},
        responsible=str(admin.get("email") or "").strip() or "Администратор системы",
        persist_now=True,
    )
    return payload


@router.get("/{invoice_id}")
def get_invoice(
    invoice_id: str,
    http_request: FastapiRequest,
    db: Session = Depends(get_db),
    admin: dict = Depends(require_role("ADMIN", "LAWYER")),
):
    role = str(admin.get("role") or "").upper()
    actor_id = _actor_uuid_or_401(admin)

    invoice = db.get(Invoice, _uuid_or_400(invoice_id, "invoice_id"))
    if invoice is None:
        raise HTTPException(status_code=404, detail="Счет не найден")

    req = db.get(Request, invoice.request_id)
    if req is None:
        raise HTTPException(status_code=404, detail="Заявка не найдена")
    _ensure_lawyer_owns_request_or_403(role, actor_id, req)

    issuer = db.get(AdminUser, invoice.issued_by_admin_user_id) if invoice.issued_by_admin_user_id else None
    payload = _serialize_invoice(
        invoice,
        request_track=req.track_number,
        issuer_name=issuer.name if issuer else None,
        include_payer_details=True,
    )
    record_pii_access_event(
        db,
        actor_role=role,
        actor_subject=str(admin.get("sub") or admin.get("email") or ""),
        actor_ip=extract_client_ip(http_request),
        action="READ_INVOICE_CARD",
        scope="INVOICE",
        request_id=req.id,
        details={"invoice_id": str(invoice.id), "invoice_number": invoice.invoice_number},
        responsible=str(admin.get("email") or "").strip() or "Администратор системы",
        persist_now=True,
    )
    return payload


@router.post("", status_code=201)
def create_invoice(
    payload: dict,
    db: Session = Depends(get_db),
    admin: dict = Depends(require_role("ADMIN", "LAWYER")),
):
    role = str(admin.get("role") or "").upper()
    actor_id = _actor_uuid_or_401(admin)
    actor_email = str(admin.get("email") or "").strip() or "Администратор системы"
    actor_user = db.get(AdminUser, actor_id)
    actor_name = str(actor_user.name if actor_user else "").strip() or str(actor_user.email if actor_user else "").strip() or actor_email

    req = _request_from_payload_or_404(db, payload)
    _ensure_lawyer_owns_request_or_403(role, actor_id, req)

    status = _normalize_status(payload.get("status"))
    if role == "LAWYER" and status == STATUS_PAID:
        raise HTTPException(status_code=403, detail='Юрист не может ставить статус "Оплачен"')

    payer_display_name = str(payload.get("payer_display_name") or "").strip()
    if not payer_display_name:
        raise HTTPException(status_code=400, detail='Поле "payer_display_name" обязательно')

    issued_at = _now_utc()
    invoice = Invoice(
        request_id=req.id,
        client_id=req.client_id,
        invoice_number=str(payload.get("invoice_number") or "").strip() or generate_invoice_number(db, issued_at),
        status=status,
        amount=_amount_or_400(payload.get("amount")),
        currency=_normalize_currency(payload.get("currency")),
        payer_display_name=payer_display_name,
        payer_details_encrypted=encrypt_requisites(_parse_requisites(payload.get("payer_details"))),
        issued_by_admin_user_id=actor_id,
        issued_by_role=role,
        issued_at=issued_at,
        paid_at=None,
        responsible=actor_email,
    )

    req.invoice_amount = invoice.amount
    req.responsible = actor_email

    if status == STATUS_PAID:
        invoice.paid_at = _now_utc()
        _apply_paid_flags(req, invoice, admin_id=actor_id if role == "ADMIN" else None)

    db.add(invoice)
    db.add(req)
    create_invoice_chat_message_with_attachment(
        db,
        request=req,
        invoice=invoice,
        actor_role=role,
        actor_name=actor_name,
        actor_admin_user_id=str(actor_id),
        responsible=actor_email,
    )
    _commit_or_400(db, "Счет с таким номером уже существует")
    db.refresh(invoice)

    issuer = db.get(AdminUser, invoice.issued_by_admin_user_id) if invoice.issued_by_admin_user_id else None
    return _serialize_invoice(invoice, request_track=req.track_number, issuer_name=issuer.name if issuer else None)


@router.patch("/{invoice_id}")
def update_invoice(
    invoice_id: str,
    payload: dict,
    db: Session = Depends(get_db),
    admin: dict = Depends(require_role("ADMIN")),
):
    role = str(admin.get("role") or "").upper()
    actor_id = _actor_uuid_or_401(admin)
    actor_email = str(admin.get("email") or "").strip() or "Администратор системы"

    invoice = db.get(Invoice, _uuid_or_400(invoice_id, "invoice_id"))
    if invoice is None:
        raise HTTPException(status_code=404, detail="Счет не найден")

    req = db.get(Request, invoice.request_id)
    if req is None:
        raise HTTPException(status_code=404, detail="Заявка не найдена")
    _ensure_lawyer_owns_request_or_403(role, actor_id, req)

    prev_status = str(invoice.status or "").upper()
    prev_paid_at = invoice.paid_at

    if "amount" in payload:
        invoice.amount = _amount_or_400(payload.get("amount"))
        req.invoice_amount = invoice.amount
        if prev_status == STATUS_PAID:
            req.paid_at = invoice.paid_at
    if "currency" in payload:
        invoice.currency = _normalize_currency(payload.get("currency"))
    if "payer_display_name" in payload:
        name = str(payload.get("payer_display_name") or "").strip()
        if not name:
            raise HTTPException(status_code=400, detail='Поле "payer_display_name" не может быть пустым')
        invoice.payer_display_name = name
    if "payer_details" in payload:
        invoice.payer_details_encrypted = encrypt_requisites(_parse_requisites(payload.get("payer_details")))
    if "invoice_number" in payload and str(payload.get("invoice_number") or "").strip():
        invoice.invoice_number = str(payload.get("invoice_number") or "").strip()

    if "status" in payload:
        next_status = _normalize_status(payload.get("status"))
        if role == "LAWYER" and next_status == STATUS_PAID:
            raise HTTPException(status_code=403, detail='Юрист не может ставить статус "Оплачен"')
        if role == "LAWYER" and prev_status == STATUS_PAID and next_status != STATUS_PAID:
            raise HTTPException(status_code=403, detail="Юрист не может менять статус уже оплаченного счета")

        invoice.status = next_status
        if next_status == STATUS_PAID:
            if role != "ADMIN":
                raise HTTPException(status_code=403, detail='Юрист не может ставить статус "Оплачен"')
            invoice.paid_at = _now_utc()
            _apply_paid_flags(req, invoice, admin_id=actor_id)
        else:
            invoice.paid_at = None
            req.invoice_amount = invoice.amount
            if prev_paid_at is not None and req.paid_at == prev_paid_at:
                req.paid_at = None
                req.paid_by_admin_id = None

    invoice.responsible = actor_email
    req.responsible = actor_email

    db.add(invoice)
    db.add(req)
    _commit_or_400(db, "Счет с таким номером уже существует")
    db.refresh(invoice)

    issuer = db.get(AdminUser, invoice.issued_by_admin_user_id) if invoice.issued_by_admin_user_id else None
    return _serialize_invoice(invoice, request_track=req.track_number, issuer_name=issuer.name if issuer else None)


@router.delete("/{invoice_id}")
def delete_invoice(
    invoice_id: str,
    db: Session = Depends(get_db),
    admin: dict = Depends(require_role("ADMIN")),
):
    actor_email = str(admin.get("email") or "").strip() or "Администратор системы"

    invoice = db.get(Invoice, _uuid_or_400(invoice_id, "invoice_id"))
    if invoice is None:
        raise HTTPException(status_code=404, detail="Счет не найден")

    req = db.get(Request, invoice.request_id)
    if req is not None:
        if invoice.paid_at is not None and req.paid_at == invoice.paid_at:
            req.paid_at = None
            req.paid_by_admin_id = None
        req.responsible = actor_email
        db.add(req)

    db.delete(invoice)
    db.commit()
    return {"status": "удалено", "id": invoice_id, "responsible": actor_email}


@router.get("/{invoice_id}/pdf")
def download_invoice_pdf(
    invoice_id: str,
    http_request: FastapiRequest,
    db: Session = Depends(get_db),
    admin: dict = Depends(require_role("ADMIN", "LAWYER")),
):
    role = str(admin.get("role") or "").upper()
    actor_id = _actor_uuid_or_401(admin)

    invoice = db.get(Invoice, _uuid_or_400(invoice_id, "invoice_id"))
    if invoice is None:
        raise HTTPException(status_code=404, detail="Счет не найден")

    req = db.get(Request, invoice.request_id)
    if req is None:
        raise HTTPException(status_code=404, detail="Заявка не найдена")
    _ensure_lawyer_owns_request_or_403(role, actor_id, req)

    issuer = db.get(AdminUser, invoice.issued_by_admin_user_id) if invoice.issued_by_admin_user_id else None
    requisites = decrypt_requisites(invoice.payer_details_encrypted)
    pdf_bytes = build_invoice_pdf_bytes(
        invoice_number=invoice.invoice_number,
        amount=_to_float(invoice.amount) or 0.0,
        currency=invoice.currency,
        status=STATUS_LABELS.get(str(invoice.status or "").upper(), invoice.status or "-"),
        issued_at=invoice.issued_at,
        paid_at=invoice.paid_at,
        payer_display_name=invoice.payer_display_name,
        request_track_number=req.track_number,
        issued_by_name=(issuer.name if issuer else None),
        requisites=requisites,
    )
    record_pii_access_event(
        db,
        actor_role=role,
        actor_subject=str(admin.get("sub") or admin.get("email") or ""),
        actor_ip=extract_client_ip(http_request),
        action="READ_INVOICE_PDF",
        scope="INVOICE",
        request_id=req.id,
        details={"invoice_id": str(invoice.id), "invoice_number": invoice.invoice_number},
        responsible=str(admin.get("email") or "").strip() or "Администратор системы",
        persist_now=True,
    )

    file_name = f"{invoice.invoice_number}.pdf"
    headers = {"Content-Disposition": f'attachment; filename="{file_name}"'}
    return StreamingResponse(iter([pdf_bytes]), media_type="application/pdf", headers=headers)
