<?php

namespace App\Services;

use App\Models\Lookups\DocumentSequence;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use InvalidArgumentException;

/**
 * Concurrency-safe document numbering.
 *
 * Replaces unsafe `max(id)+1` patterns. Uses a row-locked counter table so two
 * concurrent transactions always receive distinct numbers — works on MySQL 8
 * (InnoDB), SQLite (no-op lockForUpdate, single-writer model), and Postgres.
 *
 * Year-scoped sequences (invoice, return, credit_note, damage, sales_order,
 * return_grn) use
 * the storage key "{kind}:{year}". Global counters (transaction, claim, grn,
 * supplier_document, sales_rep) use just "{kind}".
 *
 * Format tokens:
 *   {year}       → current 4-digit year
 *   {seq}        → raw integer
 *   {seq:N}      → zero-padded to N digits
 */
class DocumentNumberService
{
    /**
     * Format presets keyed by kind. The "year_scoped" flag governs whether the
     * sequence resets each year. Callers can pass an override format if needed
     * (e.g. tenant-customised prefixes).
     */
    public const PRESETS = [
        'invoice' => ['format' => 'INV-{year}-{seq:6}',   'year_scoped' => true],
        'return' => ['format' => 'RTN-{year}{seq:5}',    'year_scoped' => true],
        'credit_note' => ['format' => 'CN-{year}{seq:5}',     'year_scoped' => true],
        'damage' => ['format' => 'DMG-{year}{seq:5}',    'year_scoped' => true],
        'sales_order' => ['format' => 'SO-{year}-{seq:5}',    'year_scoped' => true],
        'purchase_order' => ['format' => 'PO-{year}-{seq:5}',    'year_scoped' => true],
        'batch' => ['format' => 'BATCH-{seq:6}',         'year_scoped' => false],
        'transaction' => ['format' => 'TRN-{seq:6}',          'year_scoped' => false],
        'claim' => ['format' => 'CLM-{seq:6}',          'year_scoped' => false],
        'grn' => ['format' => 'DOC-{seq:5}',          'year_scoped' => false],
        'return_grn' => ['format' => 'RGRN-{year}-{seq:5}',   'year_scoped' => true],
        'grn_reference' => ['format' => 'GRN-{seq:5}',          'year_scoped' => false],
        'supplier_document' => ['format' => 'DOC-{seq:5}',          'year_scoped' => false],
        'sales_rep' => ['format' => 'REP-{seq:4}',          'year_scoped' => false],
        'till' => ['format' => 'TILL-{seq:5}',         'year_scoped' => false],
        'held_sale' => ['format' => 'H-{seq:6}',          'year_scoped' => false],
        // Shelf/stock labels the shop prints itself. Never year-scoped: a label
        // stays stuck to its stock across a new year, and the prefix must match
        // BarcodeLabel::CODE_PREFIX — the terminal recognises a label scan by it.
        'barcode_label' => ['format' => 'LBL-{seq:8}',       'year_scoped' => false],
    ];

    /**
     * Maps each kind to the [table, column] that stores its already-issued
     * numbers. Used to initialise a fresh counter (or resync a drifted one)
     * from existing data instead of restarting at 1 — which would collide with
     * seeded/imported documents.
     *
     * @var array<string,array{0:string,1:string}>
     */
    private const SOURCES = [
        'invoice' => ['invoices', 'invoice_number'],
        'return' => ['returns', 'return_number'],
        'credit_note' => ['credit_notes', 'cn_number'],
        'damage' => ['damages', 'damage_number'],
        'transaction' => ['transactions', 'transaction_number'],
        'claim' => ['transaction_claims', 'claim_number'],
        'grn' => ['grns', 'grn_number'],
        'return_grn' => ['return_grns', 'return_grn_number'],
        'supplier_document' => ['supplier_documents', 'document_number'],
        'purchase_order' => ['purchase_orders', 'po_number'],
        'held_sale' => ['held_sales', 'reference'],
        'sales_order' => ['sales_orders', 'order_number'],
        'till' => ['tills', 'identifier'],
        'barcode_label' => ['barcode_labels', 'code'],
    ];

    /**
     * Generate the next number for a kind.
     *
     * @param  string  $kind  one of the keys in PRESETS
     * @param  string|null  $formatOverride  custom format with the {year}/{seq:N} tokens
     * @param  bool|null  $yearScopedOverride  override the preset's scoping flag
     */
    public function next(string $kind, ?string $formatOverride = null, ?bool $yearScopedOverride = null): string
    {
        if (! isset(self::PRESETS[$kind])) {
            throw new InvalidArgumentException("Unknown document kind: {$kind}");
        }

        $preset = self::PRESETS[$kind];
        $format = $formatOverride ?? $preset['format'];
        $yearScoped = $yearScopedOverride ?? $preset['year_scoped'];

        $year = now()->year;
        $key = $yearScoped ? "{$kind}:{$year}" : $kind;

        $value = DB::transaction(function () use ($key, $kind) {
            $row = DocumentSequence::query()
                ->where('sequence_key', $key)
                ->lockForUpdate()
                ->first();

            $highest = $this->highestIssued($kind);
            $target = $highest + 1;

            if (! $row) {
                // First write under this key — race-safe via UNIQUE constraint.
                // Seed from the highest already-issued number so we never restart
                // at 1 and collide with seeded/imported documents.
                $row = DocumentSequence::create([
                    'sequence_key' => $key,
                    'next_value' => $target,
                ]);
            } elseif ($row->next_value < $target) {
                // Counter drifted behind already-issued records — advance it to target
                $row->update(['next_value' => $target]);
            }

            $current = (int) $row->next_value;
            $row->update(['next_value' => $current + 1]);

            return $current;
        });

        return $this->formatNumber($format, $value, $year);
    }

    /**
     * Peek at the next value without consuming it. Useful for previews and tests.
     * Not safe for production numbering (no lock).
     */
    public function peek(string $kind): int
    {
        if (! isset(self::PRESETS[$kind])) {
            throw new InvalidArgumentException("Unknown document kind: {$kind}");
        }

        $preset = self::PRESETS[$kind];
        $key = $preset['year_scoped'] ? "{$kind}:".now()->year : $kind;

        return (int) (DocumentSequence::where('sequence_key', $key)->value('next_value') ?? 1);
    }

    /**
     * Highest sequence value already used by issued documents of this kind,
     * read straight from the persisted numbers. Year-scoped kinds only consider
     * the current year. Returns 0 when the kind is unmapped or no rows exist.
     */
    public function highestIssued(string $kind): int
    {
        if (! isset(self::PRESETS[$kind], self::SOURCES[$kind])) {
            return 0;
        }

        [$table, $column] = self::SOURCES[$kind];
        if (! Schema::hasTable($table) || ! Schema::hasColumn($table, $column)) {
            return 0;
        }

        $preset = self::PRESETS[$kind];
        $width = $this->seqWidth($preset['format']);

        $query = DB::table($table)->whereNotNull($column);

        // Per-tenant high-water mark: each tenant numbers its own documents.
        $tenantId = app(CurrentContext::class)->tenantId();
        if ($tenantId !== null && Schema::hasColumn($table, 'tenant_id')) {
            $query->where('tenant_id', $tenantId);
        }

        if ($preset['year_scoped']) {
            $query->where($column, 'like', '%'.now()->year.'%');
        }

        $max = 0;
        foreach ($query->pluck($column) as $number) {
            $digits = preg_replace('/\D/', '', (string) $number);
            if ($digits === '' || $digits === null) {
                continue;
            }
            // The trailing {seq:N} digits hold the counter; slice them off the end.
            $seq = ($width > 0 && strlen($digits) > $width) ? substr($digits, -$width) : $digits;
            $max = max($max, (int) $seq);
        }

        return $max;
    }

    /**
     * Repair drifted or missing counters by advancing each to at least the
     * highest number already issued + 1. Never lowers a counter, so it is safe
     * to run repeatedly.
     *
     * @return array<string,int> changed sequence_key => new next_value
     */
    public function resync(): array
    {
        $changed = [];

        foreach (array_keys(self::SOURCES) as $kind) {
            $preset = self::PRESETS[$kind];
            $key = $preset['year_scoped'] ? "{$kind}:".now()->year : $kind;
            $target = $this->highestIssued($kind) + 1;

            $current = (int) (DocumentSequence::query()->where('sequence_key', $key)->value('next_value') ?? 0);

            if ($target > $current) {
                DocumentSequence::query()->updateOrCreate(
                    ['sequence_key' => $key],
                    ['next_value' => $target],
                );
                $changed[$key] = $target;
            }
        }

        return $changed;
    }

    /**
     * Extract the zero-pad width N from a {seq:N} token (0 when unpadded).
     */
    private function seqWidth(string $format): int
    {
        return preg_match('/\{seq:(\d+)\}/', $format, $m) ? (int) $m[1] : 0;
    }

    /**
     * Apply the {year} and {seq[:N]} substitutions.
     */
    protected function formatNumber(string $format, int $value, int $year): string
    {
        $out = str_replace('{year}', (string) $year, $format);

        $out = preg_replace_callback('/\{seq(?::(\d+))?\}/', function ($m) use ($value) {
            $pad = isset($m[1]) ? (int) $m[1] : 0;

            return $pad > 0 ? str_pad((string) $value, $pad, '0', STR_PAD_LEFT) : (string) $value;
        }, $out);

        return $out;
    }
}
