<?php

declare(strict_types=1);

namespace App\Services;

use App\Core\Auth;
use App\Core\Database;
use App\Core\Tenant;
use RuntimeException;

class BillingService
{
    public function createCreditNote(int $saleId, float $amount, string $reason): int
    {
        return $this->createNote($saleId, 'nota_credito', $amount, $reason);
    }

    public function createDebitNote(int $saleId, float $amount, string $reason): int
    {
        return $this->createNote($saleId, 'nota_debito', $amount, $reason);
    }

    public function cancelDocument(int $documentId, string $reason): int
    {
        $companyId = (int) (Tenant::companyId() ?? 0);
        $userId = (int) (Auth::id() ?? 0);

        if ($companyId <= 0 || $userId <= 0) {
            throw new RuntimeException('Sesión inválida para anular el documento.');
        }

        $normalizedReason = trim($reason);
        if ($normalizedReason === '') {
            throw new RuntimeException('Debes indicar el motivo de anulación documental.');
        }

        if ($documentId <= 0) {
            throw new RuntimeException('Documento inválido para anular.');
        }

        return Database::transaction(function () use ($companyId, $userId, $documentId, $normalizedReason): int {
            $pdo = Database::connection();

            $documentStmt = $pdo->prepare(
                'SELECT *
                 FROM facturas
                 WHERE id = :id
                   AND empresa_id = :empresa_id
                 LIMIT 1
                 FOR UPDATE'
            );
            $documentStmt->execute([
                ':id' => $documentId,
                ':empresa_id' => $companyId,
            ]);
            $document = $documentStmt->fetch();

            if (!$document) {
                throw new RuntimeException('El documento no existe en la empresa activa.');
            }

            if (($document['estado'] ?? '') === 'anulada') {
                throw new RuntimeException('El documento ya se encuentra anulado.');
            }

            $dependentStmt = $pdo->prepare(
                'SELECT COUNT(*)
                 FROM facturas
                 WHERE empresa_id = :empresa_id
                   AND documento_referencia_id = :documento_id
                   AND estado <> "anulada"'
            );
            $dependentStmt->execute([
                ':empresa_id' => $companyId,
                ':documento_id' => $documentId,
            ]);
            $dependentCount = (int) $dependentStmt->fetchColumn();

            if ($dependentCount > 0) {
                throw new RuntimeException('No se puede anular porque tiene documentos dependientes activos.');
            }

            $updateStmt = $pdo->prepare(
                'UPDATE facturas
                 SET estado = "anulada",
                     anulado_por_usuario_id = :usuario_id,
                     anulado_motivo = :motivo,
                     anulado_at = NOW(),
                     updated_at = NOW()
                 WHERE id = :id
                   AND empresa_id = :empresa_id'
            );
            $updateStmt->execute([
                ':usuario_id' => $userId,
                ':motivo' => $normalizedReason,
                ':id' => $documentId,
                ':empresa_id' => $companyId,
            ]);

            return $documentId;
        });
    }

    private function createNote(int $saleId, string $type, float $amount, string $reason): int
    {
        $companyId = (int) (Tenant::companyId() ?? 0);
        $userId = (int) (Auth::id() ?? 0);

        if ($companyId <= 0 || $userId <= 0) {
            throw new RuntimeException('Sesión inválida para emitir notas.');
        }

        if ($saleId <= 0) {
            throw new RuntimeException('Venta inválida para emitir nota.');
        }

        $normalizedType = strtolower(trim($type));
        if (!in_array($normalizedType, ['nota_credito', 'nota_debito'], true)) {
            throw new RuntimeException('Tipo de nota no soportado.');
        }

        $normalizedReason = trim($reason);
        if ($normalizedReason === '') {
            throw new RuntimeException('Debes indicar el motivo de la nota.');
        }

        $normalizedAmount = round($amount, 2);
        if ($normalizedAmount <= 0) {
            throw new RuntimeException('El valor de la nota debe ser mayor a cero.');
        }

        return Database::transaction(function () use (
            $companyId,
            $saleId,
            $normalizedType,
            $normalizedAmount,
            $normalizedReason
        ): int {
            $pdo = Database::connection();

            $saleStmt = $pdo->prepare(
                'SELECT id, empresa_id, sucursal_id, cliente_id, total, estado
                 FROM ventas
                 WHERE id = :venta_id
                   AND empresa_id = :empresa_id
                 LIMIT 1
                 FOR UPDATE'
            );
            $saleStmt->execute([
                ':venta_id' => $saleId,
                ':empresa_id' => $companyId,
            ]);
            $sale = $saleStmt->fetch();

            if (!$sale) {
                throw new RuntimeException('La venta no existe en la empresa activa.');
            }

            if (($sale['estado'] ?? '') === 'anulada') {
                throw new RuntimeException('No se pueden emitir notas para una venta anulada.');
            }

            $invoiceStmt = $pdo->prepare(
                'SELECT *
                 FROM facturas
                 WHERE empresa_id = :empresa_id
                   AND venta_id = :venta_id
                   AND tipo_documento = "factura"
                 ORDER BY id DESC
                 LIMIT 1
                 FOR UPDATE'
            );
            $invoiceStmt->execute([
                ':empresa_id' => $companyId,
                ':venta_id' => $saleId,
            ]);
            $mainInvoice = $invoiceStmt->fetch();

            if (!$mainInvoice) {
                throw new RuntimeException('La venta no tiene factura base para emitir notas.');
            }

            if (($mainInvoice['estado'] ?? '') === 'anulada') {
                throw new RuntimeException('No se pueden emitir notas sobre una factura anulada.');
            }

            $trailStmt = $pdo->prepare(
                'SELECT
                    COALESCE(SUM(CASE WHEN tipo_documento = "factura" AND estado <> "anulada" THEN total ELSE 0 END), 0) AS total_facturado,
                    COALESCE(SUM(CASE WHEN tipo_documento = "nota_credito" AND estado <> "anulada" THEN total ELSE 0 END), 0) AS total_credito,
                    COALESCE(SUM(CASE WHEN tipo_documento = "nota_debito" AND estado <> "anulada" THEN total ELSE 0 END), 0) AS total_debito
                 FROM facturas
                 WHERE empresa_id = :empresa_id
                   AND venta_id = :venta_id'
            );
            $trailStmt->execute([
                ':empresa_id' => $companyId,
                ':venta_id' => $saleId,
            ]);
            $trail = $trailStmt->fetch() ?: [];

            $totalFacturado = (float) ($trail['total_facturado'] ?? 0);
            $totalCredito = (float) ($trail['total_credito'] ?? 0);
            $totalDebito = (float) ($trail['total_debito'] ?? 0);
            $netoDocumental = $totalFacturado + $totalDebito - $totalCredito;

            if ($normalizedType === 'nota_credito' && $normalizedAmount > $netoDocumental + 0.0001) {
                throw new RuntimeException('La nota crédito no puede exceder el saldo documental vigente.');
            }

            $branchId = (int) ($sale['sucursal_id'] ?? 0);
            $documentNumber = $this->reserveDocumentNumber($pdo, $companyId, $branchId, $normalizedType, $saleId);

            $insertStmt = $pdo->prepare(
                'INSERT INTO facturas (
                    empresa_id, sucursal_id, venta_id, cliente_id, tipo_documento, numero,
                    subtotal, impuestos, total, estado,
                    documento_referencia_id, motivo_documento,
                    fecha_emision, created_at, updated_at
                ) VALUES (
                    :empresa_id, :sucursal_id, :venta_id, :cliente_id, :tipo_documento, :numero,
                    :subtotal, :impuestos, :total, :estado,
                    :documento_referencia_id, :motivo_documento,
                    NOW(), NOW(), NOW()
                )'
            );
            $insertStmt->execute([
                ':empresa_id' => $companyId,
                ':sucursal_id' => $branchId > 0 ? $branchId : null,
                ':venta_id' => $saleId,
                ':cliente_id' => $sale['cliente_id'] !== null ? (int) $sale['cliente_id'] : null,
                ':tipo_documento' => $normalizedType,
                ':numero' => $documentNumber,
                ':subtotal' => $normalizedAmount,
                ':impuestos' => 0,
                ':total' => $normalizedAmount,
                ':estado' => 'emitida',
                ':documento_referencia_id' => (int) $mainInvoice['id'],
                ':motivo_documento' => $normalizedReason,
            ]);

            $noteId = (int) $pdo->lastInsertId();

            $detailStmt = $pdo->prepare(
                'INSERT INTO factura_detalles (
                    factura_id, producto_id, descripcion, cantidad, precio_unitario, impuesto, total_linea
                ) VALUES (
                    :factura_id, NULL, :descripcion, 1, :precio_unitario, 0, :total_linea
                )'
            );
            $detailStmt->execute([
                ':factura_id' => $noteId,
                ':descripcion' => strtoupper(str_replace('_', ' ', $normalizedType)) . ': ' . $normalizedReason,
                ':precio_unitario' => $normalizedAmount,
                ':total_linea' => $normalizedAmount,
            ]);

            return $noteId;
        });
    }

    private function reserveDocumentNumber(\PDO $pdo, int $companyId, int $branchId, string $type, int $saleId): string
    {
        $stmt = $pdo->prepare(
            'SELECT *
             FROM numeraciones_documentos
             WHERE empresa_id = :empresa_id
               AND tipo_documento = :tipo_documento
               AND activo = 1
               AND (sucursal_id = :sucursal_id OR sucursal_id IS NULL)
             ORDER BY
                CASE WHEN sucursal_id = :sucursal_id_priority THEN 0 ELSE 1 END,
                id ASC
             LIMIT 1
             FOR UPDATE'
        );
        $stmt->execute([
            ':empresa_id' => $companyId,
            ':tipo_documento' => $type,
            ':sucursal_id' => $branchId,
            ':sucursal_id_priority' => $branchId,
        ]);
        $numbering = $stmt->fetch();

        if (!$numbering) {
            $fallbackPrefix = match ($type) {
                'nota_credito' => 'NC',
                'nota_debito' => 'ND',
                default => 'DOC',
            };
            return $fallbackPrefix . '-' . $saleId . '-' . date('His');
        }

        $current = (int) ($numbering['consecutivo_actual'] ?? 0);
        $final = $numbering['consecutivo_final'] !== null ? (int) $numbering['consecutivo_final'] : null;
        $prefix = trim((string) ($numbering['prefijo'] ?? ''));

        if ($current <= 0) {
            throw new RuntimeException('La numeración activa de documentos tiene consecutivo inválido.');
        }

        if ($final !== null && $current > $final) {
            throw new RuntimeException('La numeración activa ya alcanzó su consecutivo final.');
        }

        $updateStmt = $pdo->prepare(
            'UPDATE numeraciones_documentos
             SET consecutivo_actual = :siguiente,
                 updated_at = NOW()
             WHERE id = :id
               AND empresa_id = :empresa_id'
        );
        $updateStmt->execute([
            ':siguiente' => $current + 1,
            ':id' => (int) $numbering['id'],
            ':empresa_id' => $companyId,
        ]);

        return substr(($prefix !== '' ? $prefix : '') . (string) $current, 0, 80);
    }
}
