<?php
/**
 * DocNumberSettings — central atomic document numbering (Slice 8i).
 *
 * Improves on the legacy `rookpmgt_hqdspera.order_settings` table by adding:
 *   · configurable prefix + separator + year component + zero-pad width
 *   · YEARLY / MONTHLY / NEVER reset scopes with auto-detection on allocate
 *   · atomic race-safe allocate via single UPDATE with WHERE-guard + retry
 *   · idempotent seeding of known doc_keys with sane defaults
 *
 * Format template:
 *   {PREFIX}{SEP}{YEAR?}{SEP}{SEQ_PADDED}     (year_position = AFTER_PREFIX, default)
 *   {PREFIX}{SEP}{SEQ_PADDED}{SEP}{YEAR?}     (year_position = AFTER_SEQ)
 *   {PREFIX}{SEP}{YEAR?}{SEP}{SEQ_PADDED}     (year_position = BEFORE_SEQ, same as AFTER_PREFIX)
 *
 * Examples with defaults:
 *   LPO       → LPO-2026-0001
 *   IPC       → IPC-2026-0007
 *   SCRUB     → SCR-2026-0003
 *   CLIENT_PO → CPO-2026-0012
 */
class DocNumberSettings extends Model
{
    /**
     * Atomically allocate the next number for a doc_key and return the formatted string.
     *
     *   $lpoNo = (new DocNumberSettings())->allocate('LPO');   // "LPO-2026-0001"
     *
     * Race-safe via WHERE-guarded UPDATE:
     *   UPDATE ... SET next_no = next_no + 1 WHERE doc_key = ? AND next_no = ?
     * If affected_rows = 0 (someone else grabbed our seq), re-read and retry (up to 5x).
     */
    public function allocate(string $docKey): string
    {
        if ($docKey === '') throw new RuntimeException('doc_key required.');
        $db = new DB();

        for ($try = 1; $try <= 5; $try++) {
            $row = $db->mySelect("SELECT * FROM doc_number_settings WHERE doc_key = :k AND deleted = 0 LIMIT 1", [':k' => $docKey]);
            if (empty($row)) throw new RuntimeException("Unknown doc_key '$docKey' — configure it in /DocNumbers first.");
            $s = $row[0];

            // Auto-rollover if scope trips (YEARLY: year change; MONTHLY: year OR month change)
            $wantYear  = (int)date('Y');
            $wantMonth = (int)date('n');
            $needReset = false;
            if ($s['reset_scope'] === 'YEARLY'  && (int)$s['current_year']  !== $wantYear)                                       $needReset = true;
            if ($s['reset_scope'] === 'MONTHLY' && ((int)$s['current_year'] !== $wantYear || (int)$s['current_month'] !== $wantMonth)) $needReset = true;

            if ($needReset) {
                // Reset to start_no atomically; guarded so we don't double-reset in a race.
                $r = $db->insertUpdate(
                    "UPDATE doc_number_settings
                        SET next_no       = start_no,
                            current_year  = :y,
                            current_month = :m,
                            edited_on     = NOW()
                      WHERE c0d = :id
                        AND (current_year <> :y OR (reset_scope = 'MONTHLY' AND current_month <> :m))",
                    [':y' => $wantYear, ':m' => $wantMonth, ':id' => (int)$s['c0d']]
                );
                continue; // re-read fresh state
            }

            $seq        = (int)$s['next_no'];
            $formatted  = $this->format($s, $seq, $wantYear, $wantMonth);

            // Atomic bump — only succeeds if next_no still equals what we read
            $fb = $db->insertUpdate(
                "UPDATE doc_number_settings
                    SET next_no  = next_no + 1,
                        example  = :ex,
                        edited_on = NOW()
                  WHERE c0d = :id AND next_no = :seq",
                [':ex' => $formatted, ':id' => (int)$s['c0d'], ':seq' => $seq]
            );
            if ((int)($fb['success'] ?? 0) === 1) return $formatted;
            // Someone else took our slot — loop and retry
        }
        throw new RuntimeException("Failed to allocate '$docKey' after 5 retries — contention too high or seed missing.");
    }

    /** Preview the next allocated number without bumping. Handy for form previews. */
    public function peek(string $docKey): string
    {
        $db = new DB();
        $row = $db->mySelect("SELECT * FROM doc_number_settings WHERE doc_key = :k AND deleted = 0 LIMIT 1", [':k' => $docKey]);
        if (empty($row)) return '';
        $s = $row[0];
        $wantYear  = (int)date('Y');
        $wantMonth = (int)date('n');
        // Reflect a would-be reset on peek so previews match reality
        $seq = (int)$s['next_no'];
        if ($s['reset_scope'] === 'YEARLY'  && (int)$s['current_year']  !== $wantYear)                                       $seq = (int)$s['start_no'];
        if ($s['reset_scope'] === 'MONTHLY' && ((int)$s['current_year'] !== $wantYear || (int)$s['current_month'] !== $wantMonth)) $seq = (int)$s['start_no'];
        return $this->format($s, $seq, $wantYear, $wantMonth);
    }

    /** Format a doc-number string from a settings row + sequence value. Pure — no DB. */
    public function format(array $s, int $seq, int $year, int $month = 0): string
    {
        $prefix = (string)$s['prefix'];
        $sep    = (string)$s['separator'];
        $pad    = str_pad((string)$seq, max(1, (int)$s['seq_width']), '0', STR_PAD_LEFT);
        $useYr  = (int)$s['include_year'] === 1;
        $ypos   = (string)$s['year_position'];

        $parts = [$prefix];
        if ($useYr && ($ypos === 'AFTER_PREFIX' || $ypos === 'BEFORE_SEQ')) $parts[] = (string)$year;
        $parts[] = $pad;
        if ($useYr && $ypos === 'AFTER_SEQ') $parts[] = (string)$year;
        return implode($sep, array_filter($parts, fn($x) => $x !== ''));
    }

    /* ── Admin surface: read + configure ─────────────────────────────── */

    public function list(): array
    {
        $db = new DB();
        return $db->mySelect(
            "SELECT * FROM doc_number_settings WHERE deleted = 0 ORDER BY doc_key",
            []
        ) ?: [];
    }

    public function findByKey(string $docKey): ?array
    {
        $db = new DB();
        $r = $db->mySelect("SELECT * FROM doc_number_settings WHERE doc_key = :k AND deleted = 0 LIMIT 1", [':k' => $docKey]);
        return $r[0] ?? null;
    }

    /**
     * Update the mutable configuration of an existing doc_key.
     * Guards:
     *   · seq_width must be 1..12
     *   · next_no must be >= start_no (unless caller explicitly resets)
     *   · reset_scope must be a valid enum value
     *   · prefix must be ≤ 20 chars; separator ≤ 4 chars
     */
    public function configure(int $id, array $d, int $userId): array
    {
        $db = new DB();
        $ex = $db->mySelect("SELECT * FROM doc_number_settings WHERE c0d = :id AND deleted = 0", [':id' => $id]);
        if (empty($ex)) return ['ok' => false, 'msg' => 'doc_number_settings row not found.'];
        $cur = $ex[0];

        $prefix      = isset($d['prefix'])       ? substr(trim((string)$d['prefix']), 0, 20) : $cur['prefix'];
        $separator   = isset($d['separator'])    ? substr((string)$d['separator'], 0, 4)      : $cur['separator'];
        $seqWidth    = (int)($d['seq_width']     ?? $cur['seq_width']);
        $startNo     = (int)($d['start_no']      ?? $cur['start_no']);
        $nextNo      = (int)($d['next_no']       ?? $cur['next_no']);
        $includeYear = isset($d['include_year']) ? ((int)$d['include_year'] ? 1 : 0) : (int)$cur['include_year'];
        $yearPos     = in_array(($d['year_position'] ?? $cur['year_position']), ['AFTER_PREFIX','BEFORE_SEQ','AFTER_SEQ'], true)
                     ? (string)($d['year_position'] ?? $cur['year_position'])
                     : 'AFTER_PREFIX';
        $resetScope  = in_array(($d['reset_scope'] ?? $cur['reset_scope']), ['NEVER','YEARLY','MONTHLY'], true)
                     ? (string)($d['reset_scope'] ?? $cur['reset_scope'])
                     : 'YEARLY';
        $label       = isset($d['label']) ? substr(trim((string)$d['label']), 0, 120) : $cur['label'];
        $notes       = isset($d['notes']) ? substr((string)$d['notes'], 0, 500)       : $cur['notes'];

        if ($seqWidth < 1 || $seqWidth > 12) return ['ok' => false, 'msg' => 'seq_width must be 1..12.'];
        if ($startNo  < 1)                    return ['ok' => false, 'msg' => 'start_no must be >= 1.'];
        if ($nextNo   < $startNo)             return ['ok' => false, 'msg' => 'next_no cannot be less than start_no.'];

        $db->insertUpdate(
            "UPDATE doc_number_settings
                SET `label` = :lbl, `prefix` = :pfx, `separator` = :sep, `seq_width` = :sw,
                    `start_no` = :st, `next_no` = :nx, `include_year` = :iy,
                    `year_position` = :yp, `reset_scope` = :rs, `notes` = :nt,
                    `edited_by` = :u
              WHERE `c0d` = :id",
            [
                ':lbl' => $label, ':pfx' => $prefix, ':sep' => $separator, ':sw' => $seqWidth,
                ':st' => $startNo, ':nx' => $nextNo, ':iy' => $includeYear,
                ':yp' => $yearPos, ':rs' => $resetScope, ':nt' => $notes,
                ':u'  => $userId, ':id' => $id,
            ]
        );
        return ['ok' => true, 'msg' => 'Doc numbering config saved.'];
    }
}
