<?php

declare(strict_types=1);

namespace App\Services;

use App\Models\CollectorCreditOrder;
use App\Models\Credit;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

class CollectorCreditOrderService
{
    /**
     * Reordena los créditos de un collector.
     *
     * @param  int  $userId  Collector user ID
     * @param  int[]  $creditIds  Ordered array of ALL active credit IDs for this collector
     *
     * @throws \InvalidArgumentException If credit_ids don't match exactly
     */
    public function reorder(int $companyId, int $userId, array $creditIds): void
    {
        // Validar que el set es exacto: todos los créditos activos del collector
        $activeCreditsQuery = Credit::where('company_id', $companyId)
            ->where('collector_user_id', $userId)
            ->whereIn('status', Credit::ACTIVE_STATUSES)
            ->whereDoesntHave('children');

        $activeCreditIds = $activeCreditsQuery->pluck('id')->sort()->values()->all();
        $submittedIds = collect($creditIds)->sort()->values()->all();

        if ($activeCreditIds !== $submittedIds) {
            throw new \InvalidArgumentException(
                'El listado de créditos no coincide con los créditos activos del cobrador. '.
                'Esperados: '.count($activeCreditIds).', recibidos: '.count($creditIds).'.'
            );
        }

        DB::transaction(function () use ($companyId, $userId, $creditIds) {
            // Delete existing rows for this collector
            CollectorCreditOrder::where('company_id', $companyId)
                ->where('user_id', $userId)
                ->delete();

            // Bulk insert in order
            $rows = [];
            $now = now();
            foreach ($creditIds as $index => $creditId) {
                $rows[] = [
                    'company_id' => $companyId,
                    'user_id' => $userId,
                    'credit_id' => $creditId,
                    'sort_order' => $index + 1,
                    'created_at' => $now,
                    'updated_at' => $now,
                ];
            }

            if (! empty($rows)) {
                CollectorCreditOrder::insert($rows);
            }
        });
    }

    /**
     * Reemplaza un crédito por otro en el orden del collector.
     *
     * Usado cuando un crédito es renovado/refinanciado/etc.:
     * el crédito hijo hereda la posición del padre en la ruta.
     *
     * Si el crédito original no tiene entrada de orden, no hace nada.
     *
     * NOTA: Se elimina primero la fila auto-insertada por CreditObserver::created()
     * para el crédito hijo antes de hacer el UPDATE, evitando UniqueConstraintViolation
     * en la clave (company_id, user_id, credit_id).
     */
    public function replaceCredit(int $companyId, int $oldCreditId, int $newCreditId): void
    {
        DB::transaction(function () use ($companyId, $oldCreditId, $newCreditId): void {
            // Eliminar la fila que CreditObserver::created() insertó automáticamente
            // para el nuevo crédito. El padre hereda su posición, no el final de la ruta.
            CollectorCreditOrder::where('company_id', $companyId)
                ->where('credit_id', $newCreditId)
                ->delete();

            // Reasignar la posición del padre al crédito hijo.
            CollectorCreditOrder::where('company_id', $companyId)
                ->where('credit_id', $oldCreditId)
                ->update([
                    'credit_id' => $newCreditId,
                    'updated_at' => now(),
                ]);
        });
    }

    /**
     * Agrega un crédito al final de la ruta de un collector.
     *
     * Si el collector no tiene filas de orden aún, no hace nada
     * (se inicializará la próxima vez que acceda a la vista de orden).
     */
    public function appendCredit(int $companyId, int $userId, int $creditId): void
    {
        $hasRows = CollectorCreditOrder::where('company_id', $companyId)
            ->where('user_id', $userId)
            ->exists();

        if (! $hasRows) {
            return;
        }

        // Verificar que no exista ya
        $exists = CollectorCreditOrder::where('company_id', $companyId)
            ->where('user_id', $userId)
            ->where('credit_id', $creditId)
            ->exists();

        if ($exists) {
            return;
        }

        $maxOrder = CollectorCreditOrder::where('company_id', $companyId)
            ->where('user_id', $userId)
            ->max('sort_order') ?? 0;

        CollectorCreditOrder::create([
            'company_id' => $companyId,
            'user_id' => $userId,
            'credit_id' => $creditId,
            'sort_order' => $maxOrder + 1,
        ]);
    }

    /**
     * Inicializa order rows para un collector que aún no tiene filas.
     * Útil para migración incremental: la primera vez que un collector
     * accede a su lista, se crean las filas con orden por defecto.
     */
    public function initializeForCollector(int $companyId, int $userId): void
    {
        $hasRows = CollectorCreditOrder::where('company_id', $companyId)
            ->where('user_id', $userId)
            ->exists();

        if ($hasRows) {
            return;
        }

        $creditIds = Credit::where('company_id', $companyId)
            ->where('collector_user_id', $userId)
            ->whereIn('status', Credit::ACTIVE_STATUSES)
            ->whereDoesntHave('children')
            ->orderBy('created_at', 'desc')
            ->pluck('id')
            ->all();

        if (empty($creditIds)) {
            return;
        }

        $rows = [];
        $now = now();
        foreach ($creditIds as $index => $creditId) {
            $rows[] = [
                'company_id' => $companyId,
                'user_id' => $userId,
                'credit_id' => $creditId,
                'sort_order' => $index + 1,
                'created_at' => $now,
                'updated_at' => $now,
            ];
        }

        CollectorCreditOrder::insert($rows);
    }

    /**
     * Agrega un crédito a la ruta del collector de forma atómica e idempotente,
     * respetando el historial de posición del cliente.
     *
     * Orden de prioridad para la posición de inserción:
     *   1. Crédito hermano activo del mismo cliente → insertar justo después.
     *   2. Posición histórica guardada (última vez que ese cliente estuvo en la ruta).
     *   3. Default: al final de la lista.
     *
     * Thread-safety: lockForUpdate() serializa inserciones concurrentes para el
     * mismo (company_id, user_id). El increment() posterior usa el mismo lock de la
     * transacción, por lo que no puede ocurrir una race condition.
     *
     * Única fuente de verdad para "entrar a la ruta respetando historial" — usada
     * tanto por `CreditObserver` (cambios síncronos: creación, cambio de collector,
     * reactivación manual) como por `CollectorSyncService` (cambios de estado
     * automáticos: reconciliación nocturna, `SyncCreditStatusJob`). Antes,
     * `CollectorSyncService` usaba `appendCredit()` (sin historial ni manejo del
     * caso "ruta vacía"), así que un crédito reactivado automáticamente perdía su
     * slot histórico y, si el cobrador no tenía ninguna fila aún, ni siquiera
     * entraba a la ruta (#129).
     */
    public function insertRespectingHistory(int $companyId, int $userId, int $creditId, int $clientId): void
    {
        DB::transaction(function () use ($companyId, $userId, $creditId, $clientId): void {
            // Lock exclusivo sobre el conjunto de filas del collector.
            $existingRows = CollectorCreditOrder::where('company_id', $companyId)
                ->where('user_id', $userId)
                ->lockForUpdate()
                ->get(['id', 'credit_id', 'sort_order']);

            // Idempotencia: si ya existe la fila, no duplicar.
            if ($existingRows->contains('credit_id', $creditId)) {
                return;
            }

            $maxOrder = $existingRows->max('sort_order') ?? 0;
            $targetOrder = $this->resolveTargetOrder($companyId, $userId, $clientId, $maxOrder, $existingRows);

            // Desplazar hacia abajo las filas que quedan por delante del punto de inserción.
            if ($targetOrder <= $maxOrder) {
                DB::table('collector_credit_order')
                    ->where('company_id', $companyId)
                    ->where('user_id', $userId)
                    ->where('sort_order', '>=', $targetOrder)
                    ->increment('sort_order');
            }

            CollectorCreditOrder::create([
                'company_id' => $companyId,
                'user_id' => $userId,
                'credit_id' => $creditId,
                'sort_order' => $targetOrder,
            ]);
        });
    }

    /**
     * Determina la posición objetivo para insertar un crédito en la ruta del collector.
     *
     * @param  Collection  $existingRows  Filas actuales (ya con lockForUpdate).
     */
    private function resolveTargetOrder(
        int $companyId,
        int $userId,
        int $clientId,
        int $maxOrder,
        Collection $existingRows,
    ): int {
        if ($existingRows->isEmpty()) {
            return 1;
        }

        // Prioridad 1: crédito hermano activo del mismo cliente ya en la ruta.
        // Insertamos justo después del último crédito activo de ese cliente.
        $siblingMax = DB::table('collector_credit_order as cco')
            ->join('credits as c', 'c.id', '=', 'cco.credit_id')
            ->where('cco.company_id', $companyId)
            ->where('cco.user_id', $userId)
            ->where('c.client_id', $clientId)
            ->whereIn('cco.credit_id', $existingRows->pluck('credit_id')->all())
            ->max('cco.sort_order');

        if ($siblingMax !== null) {
            return (int) $siblingMax + 1;
        }

        // Prioridad 2: posición histórica guardada cuando el último crédito
        // de este cliente salió de la ruta.
        $historical = DB::table('collector_client_route_history')
            ->where('company_id', $companyId)
            ->where('user_id', $userId)
            ->where('client_id', $clientId)
            ->value('sort_order');

        if ($historical !== null) {
            // Nunca exceder max+1 (la ruta puede haberse encogido desde entonces).
            return min((int) $historical, $maxOrder + 1);
        }

        // Prioridad 3: al final.
        return $maxOrder + 1;
    }

    /**
     * Elimina la fila de orden de un crédito, guarda la posición histórica del cliente,
     * y re-indexa el collector de forma atómica.
     *
     * Única fuente de verdad para "salir de la ruta" — ver nota de
     * `insertRespectingHistory()`. Antes, `CollectorSyncService` hacía un
     * `delete()` crudo sin guardar el historial ni reindexar `sort_order`,
     * dejando huecos en la secuencia y perdiendo el slot del cliente para una
     * futura reactivación (#129).
     */
    public function removeAndReindex(int $companyId, int $userId, int $creditId, int $clientId): void
    {
        DB::transaction(function () use ($companyId, $userId, $creditId, $clientId): void {
            $this->saveHistoricalPosition($companyId, $userId, $creditId, $clientId);

            CollectorCreditOrder::where('company_id', $companyId)
                ->where('user_id', $userId)
                ->where('credit_id', $creditId)
                ->delete();

            $this->reindexCollector($companyId, $userId);
        });
    }

    /**
     * Persiste (o actualiza) la última posición en ruta de un cliente para un collector dado.
     * Se llama justo antes de eliminar la fila de collector_credit_order.
     */
    public function saveHistoricalPosition(int $companyId, int $userId, int $creditId, int $clientId): void
    {
        $sortOrder = CollectorCreditOrder::where('company_id', $companyId)
            ->where('user_id', $userId)
            ->where('credit_id', $creditId)
            ->value('sort_order');

        if ($sortOrder === null) {
            return; // El crédito no estaba en la ruta del collector.
        }

        DB::table('collector_client_route_history')->upsert(
            [
                'company_id' => $companyId,
                'user_id' => $userId,
                'client_id' => $clientId,
                'sort_order' => $sortOrder,
                'updated_at' => now(),
            ],
            ['company_id', 'user_id', 'client_id'],
            ['sort_order', 'updated_at'],
        );
    }

    /**
     * Re-indexa sort_order secuencialmente (1..N) para un collector.
     *
     * Usa ROW_NUMBER() OVER (window function nativa de MariaDB 10.6+)
     * en lugar de variables de sesión @rownum, que no son thread-safe.
     *
     * Thread-safety:
     * - InnoDB usa semi-consistent read para sentencias UPDATE: el subquery
     *   materializado ve el estado committed más reciente antes de adquirir locks.
     * - El UPDATE...JOIN adquiere exclusive row locks sobre todas las filas del
     *   collector, serializando re-indexados concurrentes del mismo (company_id, user_id).
     * - Llamar este método dentro de DB::transaction garantiza atomicidad
     *   con la operación previa (DELETE).
     */
    public function reindexCollector(int $companyId, int $userId): void
    {
        DB::statement(
            'UPDATE collector_credit_order cco
             JOIN (
                 SELECT id,
                        ROW_NUMBER() OVER (
                            PARTITION BY company_id, user_id
                            ORDER BY sort_order ASC
                        ) AS new_order
                 FROM collector_credit_order
                 WHERE company_id = ? AND user_id = ?
             ) ranked ON cco.id = ranked.id
             SET cco.sort_order = ranked.new_order,
                 cco.updated_at = NOW()',
            [$companyId, $userId]
        );
    }
}
