<?php
// ============================================================
// Shared helper functions
// ============================================================

function e($str) { return htmlspecialchars($str ?? '', ENT_QUOTES, 'UTF-8'); }

function flash($type, $msg) { $_SESSION['flash'][] = ['type' => $type, 'msg' => $msg]; }

function get_flashes() {
    $f = $_SESSION['flash'] ?? [];
    unset($_SESSION['flash']);
    return $f;
}

function redirect($path) {
    header('Location: ' . BASE_URL . ltrim($path, '/'));
    exit;
}

// ---------------------------------------------------------
// Document number generator, e.g. PR-2026-0001, SR-2026-0001, PO-2026-0001, GRN-2026-0001
// ---------------------------------------------------------
function generate_doc_number(PDO $pdo, $table, $column, $prefix) {
    $year = date('Y');
    $stmt = $pdo->prepare("SELECT COUNT(*) c FROM $table WHERE $column LIKE ?");
    $stmt->execute(["$prefix-$year-%"]);
    $count = (int)$stmt->fetch()['c'] + 1;
    return sprintf('%s-%s-%04d', $prefix, $year, $count);
}

// ---------------------------------------------------------
// Email + in-app notifications
// ---------------------------------------------------------

// Low-level mail sender. Routes through MAIL_DRIVER ('php_mail' or 'smtp').
// For 'smtp', SMTP_PROFILE picks between your production SMTP server and
// Mailtrap (see config/mail_config.php). Failures are logged but never break
// the app flow - notifications still land in-app either way.
function send_email($to, $subject, $bodyHtml) {
    if (!MAIL_NOTIFICATIONS_ENABLED || empty($to)) return false;

    try {
        if (MAIL_DRIVER === 'smtp') {
            if (SMTP_PROFILE === 'mailtrap') {
                $mailer = new SimpleSmtpMailer(MAILTRAP_HOST, MAILTRAP_PORT, MAILTRAP_USERNAME, MAILTRAP_PASSWORD, MAILTRAP_ENCRYPTION);
            } else {
                $mailer = new SimpleSmtpMailer(SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD, SMTP_ENCRYPTION);
            }
            $ok = $mailer->send(MAIL_FROM_ADDRESS, MAIL_FROM_NAME, $to, $subject, $bodyHtml);
            if (!$ok) error_log('SMTP send failed: ' . $mailer->lastError);
            return $ok;
        }

        // Default: PHP's built-in mail()
        $headers = "MIME-Version: 1.0\r\n";
        $headers .= "Content-type: text/html; charset=UTF-8\r\n";
        $headers .= "From: " . MAIL_FROM_NAME . " <" . MAIL_FROM_ADDRESS . ">\r\n";
        return @mail($to, $subject, $bodyHtml, $headers);
    } catch (Exception $e) {
        error_log('send_email failed: ' . $e->getMessage());
        return false;
    }
}

// Record an in-app notification (shows in the bell icon / notifications.php)
function add_notification(PDO $pdo, $userId, $message, $link = null, $module = null, $refId = null) {
    $pdo->prepare("INSERT INTO notifications (user_id, module, reference_id, message, link) VALUES (?,?,?,?,?)")
        ->execute([$userId, $module, $refId, $message, $link]);
}

function unread_notification_count(PDO $pdo, $userId) {
    $stmt = $pdo->prepare("SELECT COUNT(*) FROM notifications WHERE user_id = ? AND is_read = 0");
    $stmt->execute([$userId]);
    return (int)$stmt->fetchColumn();
}

// Builds a detailed HTML email for a PR/SR approval-workflow event - proper
// header, the actual line items (so a reviewer can judge it without opening
// the app), requester, total cost, and a clear call-to-action button.
// Returns [subject, htmlBody].
function build_approval_email(PDO $pdo, $kind, $module, $refId, $docNumber, $link, $extra = []) {
    $moduleLabel = $module === 'purchase_requisition' ? 'Purchase Requisition' : 'Store Requisition';
    $table = $module === 'purchase_requisition' ? 'purchase_requisitions' : 'store_requisitions';

    $stmt = $pdo->prepare("SELECT r.*, u.username requester FROM $table r JOIN users u ON u.id = r.requested_by WHERE r.id = ?");
    $stmt->execute([$refId]);
    $rec = $stmt->fetch();
    if (!$rec) return [APP_NAME . " - $docNumber", "<p>$docNumber - see the system for details.</p>"];

    if ($module === 'purchase_requisition') {
        $lineStmt = $pdo->prepare("SELECT i.name, pri.quantity, un.abbreviation, pri.estimated_unit_cost
                                    FROM purchase_requisition_items pri JOIN items i ON i.id = pri.item_id
                                    LEFT JOIN units un ON un.id = i.unit_id WHERE pri.pr_id = ?");
    } else {
        $lineStmt = $pdo->prepare("SELECT i.name, sri.quantity_requested quantity, un.abbreviation, NULL estimated_unit_cost
                                    FROM store_requisition_items sri JOIN items i ON i.id = sri.item_id
                                    LEFT JOIN units un ON un.id = i.unit_id WHERE sri.sr_id = ?");
    }
    $lineStmt->execute([$refId]);
    $lines = $lineStmt->fetchAll();

    $rows = '';
    foreach ($lines as $l) {
        $qty = number_format($l['quantity'], 2) . ' ' . e($l['abbreviation']);
        $cost = $l['estimated_unit_cost'] !== null ? '₦' . number_format($l['estimated_unit_cost'], 2) : '—';
        $rows .= "<tr><td style=\"padding:6px 10px;border-bottom:1px solid #e2e8f0;\">" . e($l['name']) . "</td>"
               . "<td style=\"padding:6px 10px;border-bottom:1px solid #e2e8f0;\">$qty</td>"
               . "<td style=\"padding:6px 10px;border-bottom:1px solid #e2e8f0;\">$cost</td></tr>";
    }
    $itemsTable = $lines ? "
      <table style=\"width:100%;border-collapse:collapse;margin:16px 0;font-size:13px;\">
        <thead><tr style=\"background:#f1f5f9;text-align:left;\">
          <th style=\"padding:6px 10px;\">Item</th><th style=\"padding:6px 10px;\">Qty</th><th style=\"padding:6px 10px;\">Est. Cost</th>
        </tr></thead><tbody>$rows</tbody></table>" : '';

    $fullLink = APP_URL . ltrim($link, '/');
    $amountLine = ($rec['total_estimated_cost'] ?? 0) > 0
        ? "<p style=\"margin:4px 0;\"><strong>Total estimated cost:</strong> ₦" . number_format($rec['total_estimated_cost'], 2) . "</p>" : '';

    switch ($kind) {
        case 'pending_approval':
            $stage = $extra['stage'] ?? '?';
            $subject = "$moduleLabel $docNumber needs your approval";
            $intro = "<p style=\"margin:0 0 4px;\">A $moduleLabel from <strong>" . e($rec['requester']) . "</strong> is waiting for your approval at <strong>stage $stage</strong> of the approval chain.</p>";
            $button = 'Review &amp; Approve';
            break;
        case 'approved':
            $subject = "$moduleLabel $docNumber was fully approved";
            $intro = "<p style=\"margin:0 0 4px;\">Good news — your $moduleLabel has cleared every approval stage and is now <strong>fully approved</strong>.</p>";
            $button = 'View Requisition';
            break;
        case 'rejected':
        default:
            $subject = "$moduleLabel $docNumber was rejected";
            $comment = !empty($extra['comment']) ? "<p style=\"margin:8px 0;padding:10px 12px;background:#fef2f2;border-left:3px solid #ef4444;color:#7f1d1d;\">\"" . e($extra['comment']) . "\"</p>" : '';
            $intro = "<p style=\"margin:0 0 4px;\">Your $moduleLabel was <strong>rejected</strong>.</p>$comment";
            $button = 'View Requisition';
            break;
    }

    $body = "
    <div style=\"font-family:Arial,Helvetica,sans-serif;color:#1e293b;max-width:560px;\">
      <div style=\"background:#0f172a;color:#fff;padding:14px 20px;\"><strong>" . APP_NAME . "</strong></div>
      <div style=\"padding:20px;border:1px solid #e2e8f0;border-top:none;\">
        <h2 style=\"margin:0 0 12px;font-size:18px;\">$docNumber &middot; $moduleLabel</h2>
        $intro
        <p style=\"margin:4px 0;\"><strong>Requested by:</strong> " . e($rec['requester']) . "</p>
        $amountLine
        $itemsTable
        <a href=\"$fullLink\" style=\"display:inline-block;margin-top:12px;padding:10px 18px;background:#1d4ed8;color:#fff;text-decoration:none;border-radius:6px;font-size:14px;\">$button</a>
        <p style=\"margin-top:20px;font-size:12px;color:#94a3b8;\">This is an automated notification from " . APP_NAME . ". If the button doesn't work, copy this link: $fullLink</p>
      </div>
    </div>";

    return [$subject, $body];
}

// Notify every user holding a given role (email + in-app), scoped to the
// requisition's own department. admin/superadmin and the original requester
// are always included; anyone else only gets notified if they share the
// requisition's department or hold the "view all departments" permission -
// otherwise every "supervisor" hotel-wide would get pinged about every
// department's requisitions, which is the cross-department leak this fixes.
// $richEmail, if given, is [subject, htmlBody] and overrides the plain
// one-line email format (used for PR/SR approval events); callers that don't
// pass it (e.g. return_to_supplier, waste) keep the original simple format.
function notify_role(PDO $pdo, $role, $message, $link, $module = null, $refId = null, $deptId = null, $requestedBy = null, $richEmail = null) {
    $stmt = $pdo->prepare("SELECT * FROM users WHERE role = ? OR role IN ('admin','superadmin')");
    $stmt->execute([$role]);
    $users = $stmt->fetchAll();
    $fullLink = APP_URL . ltrim($link, '/');
    foreach ($users as $usr) {
        if ($deptId !== null && !in_array($usr['role'], ['admin', 'superadmin']) && (int)$usr['id'] !== (int)$requestedBy) {
            $canSeeAll = has_permission($pdo, $usr, 'requisitions.view_all');
            $sameDept = $usr['department_id'] !== null && (int)$usr['department_id'] === (int)$deptId;
            if (!$canSeeAll && !$sameDept) continue;
        }
        add_notification($pdo, $usr['id'], $message, $link, $module, $refId);
        if (!empty($usr['email'])) {
            if ($richEmail) {
                send_email($usr['email'], $richEmail[0], $richEmail[1]);
            } else {
                $body = "<p>$message</p><p><a href=\"$fullLink\">View in Inventory System</a></p>";
                send_email($usr['email'], APP_NAME . ' - Action Required', $body);
            }
        }
    }
}

// Notify a single user (e.g. the original requester) by their user id.
// $richEmail works the same as in notify_role() above.
function notify_user(PDO $pdo, $userId, $message, $link, $module = null, $refId = null, $richEmail = null) {
    $stmt = $pdo->prepare("SELECT email FROM users WHERE id = ?");
    $stmt->execute([$userId]);
    $email = $stmt->fetchColumn();
    add_notification($pdo, $userId, $message, $link, $module, $refId);
    if ($email) {
        if ($richEmail) {
            send_email($email, $richEmail[0], $richEmail[1]);
        } else {
            $fullLink = APP_URL . ltrim($link, '/');
            $body = "<p>$message</p><p><a href=\"$fullLink\">View in Inventory System</a></p>";
            send_email($email, APP_NAME . ' - Update', $body);
        }
    }
}

// ---------------------------------------------------------
// Approval workflow engine
// ---------------------------------------------------------

// Returns ordered list of stages [ ['stage_order'=>1,'role_required'=>'supervisor'], ... ]
function get_workflow_stages(PDO $pdo, $module, $amount = 0) {
    $stmt = $pdo->prepare("SELECT * FROM approval_workflows
                            WHERE module = ? AND is_active = 1
                              AND min_amount <= ?
                              AND (max_amount IS NULL OR max_amount >= ?)
                            ORDER BY stage_order ASC");
    $stmt->execute([$module, $amount, $amount]);
    return $stmt->fetchAll();
}

function role_for_stage(PDO $pdo, $module, $stage, $amount = 0) {
    $stages = get_workflow_stages($pdo, $module, $amount);
    foreach ($stages as $s) {
        if ((int)$s['stage_order'] === (int)$stage) return $s['role_required'];
    }
    return null;
}

function max_stage(PDO $pdo, $module, $amount = 0) {
    $stages = get_workflow_stages($pdo, $module, $amount);
    if (empty($stages)) return 0;
    return max(array_column($stages, 'stage_order'));
}

// Can the current user approve this record at its current stage?
function can_approve(PDO $pdo, $module, $currentStage, $amount, $userRole) {
    if (in_array($userRole, ['superadmin'])) return true; // superadmin can always override
    $requiredRole = role_for_stage($pdo, $module, $currentStage, $amount);
    if (!$requiredRole) return false;
    return $userRole === $requiredRole || $userRole === 'admin';
}

// Which department a requisition record belongs to, for scoping notifications.
// PR carries department_id directly; SR carries it via its outlet.
function get_requisition_department_id(PDO $pdo, $module, $rec) {
    if ($module === 'purchase_requisition') return $rec['department_id'] ?? null;
    if ($module === 'store_requisition') {
        $stmt = $pdo->prepare("SELECT department_id FROM outlets WHERE id = ?");
        $stmt->execute([$rec['outlet_id']]);
        $dept = $stmt->fetchColumn();
        return $dept !== false ? $dept : null;
    }
    return null;
}

// Record an approval / rejection action and advance / close the requisition.
// Also fires email + in-app notifications to the requester and/or next approvers.
function process_approval(PDO $pdo, $module, $table, $id, $userId, $userRole, $action, $comments = '') {
    $stmt = $pdo->prepare("SELECT * FROM $table WHERE id = ?");
    $stmt->execute([$id]);
    $rec = $stmt->fetch();
    if (!$rec) return [false, 'Record not found'];

    $amount = $rec['total_estimated_cost'] ?? 0;
    if (!can_approve($pdo, $module, $rec['current_stage'], $amount, $userRole)) {
        return [false, 'You are not authorized to act on this at its current stage'];
    }

    $numberCol = $module === 'purchase_requisition' ? 'pr_number' : 'sr_number';
    $docNumber = $rec[$numberCol] ?? "#$id";
    $folder = $module === 'purchase_requisition' ? 'purchase_requisitions' : 'store_requisitions';
    $link = "$folder/view.php?id=$id";
    $deptId = get_requisition_department_id($pdo, $module, $rec);

    $log = $pdo->prepare("INSERT INTO approval_logs (module, reference_id, stage, approver_id, action, comments)
                           VALUES (?,?,?,?,?,?)");
    $log->execute([$module, $id, $rec['current_stage'], $userId, $action, $comments]);

    if ($action === 'rejected') {
        $pdo->prepare("UPDATE $table SET status = 'rejected' WHERE id = ?")->execute([$id]);
        $rich = build_approval_email($pdo, 'rejected', $module, $id, $docNumber, $link, ['comment' => $comments]);
        notify_user($pdo, $rec['requested_by'], "$docNumber was rejected" . ($comments ? " (\"$comments\")" : "") . ".", $link, $module, $id, $rich);
        return [true, 'Rejected'];
    }

    $maxStage = max_stage($pdo, $module, $amount);
    if ((int)$rec['current_stage'] >= $maxStage) {
        $pdo->prepare("UPDATE $table SET status = 'approved' WHERE id = ?")->execute([$id]);
        $rich = build_approval_email($pdo, 'approved', $module, $id, $docNumber, $link);
        notify_user($pdo, $rec['requested_by'], "$docNumber has been fully approved.", $link, $module, $id, $rich);
        return [true, 'Fully approved'];
    } else {
        $nextStage = (int)$rec['current_stage'] + 1;
        $pdo->prepare("UPDATE $table SET current_stage = current_stage + 1 WHERE id = ?")->execute([$id]);
        notify_stage_approvers($pdo, $module, $docNumber, $id, $nextStage, $amount, $link, $deptId, $rec['requested_by']);
        return [true, 'Approved, moved to next stage'];
    }
}

// Notify whoever holds the required role for a given stage that a document needs their approval
function notify_stage_approvers(PDO $pdo, $module, $docNumber, $id, $stage, $amount, $link, $deptId = null, $requestedBy = null) {
    $role = role_for_stage($pdo, $module, $stage, $amount);
    if (!$role) return;
    $rich = build_approval_email($pdo, 'pending_approval', $module, $id, $docNumber, $link, ['stage' => $stage]);
    notify_role($pdo, $role, "$docNumber requires your approval (Stage $stage).", $link, $module, $id, $deptId, $requestedBy, $rich);
}

// ---------------------------------------------------------
// Stock ledger helpers
// ---------------------------------------------------------

// Add stock into Main Store (e.g. from GRN) with moving-average cost.
// Pass $expiryDate (Y-m-d) to also record a trackable batch for expiry alerts.
function main_store_stock_in(PDO $pdo, $itemId, $qty, $unitCost, $refType, $refId, $userId, $notes = '', $expiryDate = null, $receivedDate = null) {
    $stmt = $pdo->prepare("SELECT * FROM main_store_stock WHERE item_id = ?");
    $stmt->execute([$itemId]);
    $row = $stmt->fetch();

    if ($row) {
        $newQty = $row['quantity'] + $qty;
        $newAvg = $newQty > 0
            ? (($row['quantity'] * $row['avg_cost']) + ($qty * $unitCost)) / $newQty
            : 0;
        $pdo->prepare("UPDATE main_store_stock SET quantity = ?, avg_cost = ? WHERE item_id = ?")
            ->execute([$newQty, $newAvg, $itemId]);
    } else {
        $pdo->prepare("INSERT INTO main_store_stock (item_id, quantity, avg_cost) VALUES (?,?,?)")
            ->execute([$itemId, $qty, $unitCost]);
    }

    $pdo->prepare("INSERT INTO stock_transactions
        (item_id, location_type, location_id, txn_type, quantity, unit_cost, reference_type, reference_id, created_by, notes)
        VALUES (?, 'main_store', NULL, 'grn_in', ?, ?, ?, ?, ?, ?)")
        ->execute([$itemId, $qty, $unitCost, $refType, $refId, $userId, $notes]);

    if ($expiryDate) {
        $grnId = $refType === 'GRN' ? $refId : null;
        add_item_batch($pdo, $itemId, $qty, $unitCost, $expiryDate, $receivedDate ?: date('Y-m-d'), $grnId);
    }
}

// Remove stock from Main Store and add to an Outlet (issuing a Store Requisition)
function transfer_main_to_outlet(PDO $pdo, $itemId, $outletId, $qty, $refType, $refId, $userId, $notes = '') {
    $stmt = $pdo->prepare("SELECT * FROM main_store_stock WHERE item_id = ? FOR UPDATE");
    $stmt->execute([$itemId]);
    $row = $stmt->fetch();
    $available = $row['quantity'] ?? 0;
    $unitCost = $row['avg_cost'] ?? 0;

    if ($available < $qty) {
        return [false, "Insufficient main store stock (available: $available)"];
    }

    $pdo->prepare("UPDATE main_store_stock SET quantity = quantity - ? WHERE item_id = ?")
        ->execute([$qty, $itemId]);
    consume_from_batches($pdo, $itemId, $qty);

    $pdo->prepare("INSERT INTO stock_transactions
        (item_id, location_type, location_id, txn_type, quantity, unit_cost, reference_type, reference_id, created_by, notes)
        VALUES (?, 'main_store', NULL, 'transfer_out', ?, ?, ?, ?, ?, ?)")
        ->execute([$itemId, $qty, $unitCost, $refType, $refId, $userId, $notes]);

    $stmt = $pdo->prepare("SELECT * FROM outlet_stock WHERE outlet_id = ? AND item_id = ?");
    $stmt->execute([$outletId, $itemId]);
    $orow = $stmt->fetch();
    if ($orow) {
        $pdo->prepare("UPDATE outlet_stock SET quantity = quantity + ? WHERE outlet_id = ? AND item_id = ?")
            ->execute([$qty, $outletId, $itemId]);
    } else {
        $pdo->prepare("INSERT INTO outlet_stock (outlet_id, item_id, quantity) VALUES (?,?,?)")
            ->execute([$outletId, $itemId, $qty]);
    }

    $pdo->prepare("INSERT INTO stock_transactions
        (item_id, location_type, location_id, txn_type, quantity, unit_cost, reference_type, reference_id, created_by, notes)
        VALUES (?, 'outlet', ?, 'transfer_in', ?, ?, ?, ?, ?, ?)")
        ->execute([$itemId, $outletId, $qty, $unitCost, $refType, $refId, $userId, $notes]);

    return [true, 'OK'];
}

// Remove stock from Main Store because it is being returned to a supplier
function main_store_return_out(PDO $pdo, $itemId, $qty, $unitCost, $refId, $userId, $notes = '') {
    $stmt = $pdo->prepare("SELECT * FROM main_store_stock WHERE item_id = ? FOR UPDATE");
    $stmt->execute([$itemId]);
    $row = $stmt->fetch();
    $available = $row['quantity'] ?? 0;

    if ($available < $qty) {
        return [false, "Insufficient main store stock to return (available: $available)"];
    }

    $pdo->prepare("UPDATE main_store_stock SET quantity = quantity - ? WHERE item_id = ?")->execute([$qty, $itemId]);
    consume_from_batches($pdo, $itemId, $qty);

    $pdo->prepare("INSERT INTO stock_transactions
        (item_id, location_type, location_id, txn_type, quantity, unit_cost, reference_type, reference_id, created_by, notes)
        VALUES (?, 'main_store', NULL, 'return_out', ?, ?, 'RETURN', ?, ?, ?)")
        ->execute([$itemId, $qty, $unitCost, $refId, $userId, $notes]);

    return [true, 'OK'];
}

// ---------------------------------------------------------
// Auto-generated item codes, e.g. ITM-00001
// ---------------------------------------------------------
function generate_item_code(PDO $pdo, $itemId) {
    return 'ITM-' . str_pad($itemId, 5, '0', STR_PAD_LEFT);
}

// ---------------------------------------------------------
// Expiry batch tracking
// ---------------------------------------------------------

// Record a new batch of stock with its own expiry date (called when
// receiving goods, or from a manual stock-in adjustment with an expiry date)
function add_item_batch(PDO $pdo, $itemId, $qty, $unitCost, $expiryDate, $receivedDate, $grnId = null) {
    $pdo->prepare("INSERT INTO item_batches (item_id, grn_id, quantity_received, quantity_remaining, unit_cost, expiry_date, received_date)
                   VALUES (?,?,?,?,?,?,?)")
        ->execute([$itemId, $grnId, $qty, $qty, $unitCost, $expiryDate ?: null, $receivedDate]);
}

// Consume stock from the oldest-expiring batches first (FIFO by expiry date),
// so batch "remaining" quantities stay roughly accurate for expiry alerts.
// Best-effort: if there isn't enough batch history (e.g. item never tracked
// batches before), it simply consumes whatever batches exist and stops.
function consume_from_batches(PDO $pdo, $itemId, $qty) {
    $stmt = $pdo->prepare("SELECT id, quantity_remaining FROM item_batches
                            WHERE item_id = ? AND quantity_remaining > 0
                            ORDER BY (expiry_date IS NULL), expiry_date ASC, received_date ASC, id ASC
                            FOR UPDATE");
    $stmt->execute([$itemId]);
    $batches = $stmt->fetchAll();

    $remaining = $qty;
    $upd = $pdo->prepare("UPDATE item_batches SET quantity_remaining = ? WHERE id = ?");
    foreach ($batches as $b) {
        if ($remaining <= 0) break;
        $take = min($b['quantity_remaining'], $remaining);
        $upd->execute([$b['quantity_remaining'] - $take, $b['id']]);
        $remaining -= $take;
    }
}

// Batches expiring within the next N days (or already expired), with item info
function get_expiring_batches(PDO $pdo, $days = 30) {
    $stmt = $pdo->prepare("SELECT ib.*, i.name item_name, i.code item_code, u.abbreviation unit_abbr
                            FROM item_batches ib
                            JOIN items i ON i.id = ib.item_id
                            LEFT JOIN units u ON u.id = i.unit_id
                            WHERE ib.quantity_remaining > 0 AND ib.expiry_date IS NOT NULL
                              AND ib.expiry_date <= DATE_ADD(CURDATE(), INTERVAL ? DAY)
                            ORDER BY ib.expiry_date ASC");
    $stmt->execute([$days]);
    return $stmt->fetchAll();
}

// ---------------------------------------------------------
// Waste / Spoilage write-offs
// ---------------------------------------------------------

// Write off stock from Main Store as waste (expired, damaged, spoiled, etc.)
function main_store_waste_out(PDO $pdo, $itemId, $qty, $unitCost, $refId, $userId, $notes = '') {
    $stmt = $pdo->prepare("SELECT quantity FROM main_store_stock WHERE item_id = ? FOR UPDATE");
    $stmt->execute([$itemId]);
    $available = $stmt->fetchColumn() ?: 0;

    if ($available < $qty) {
        return [false, "Insufficient main store stock to write off (available: $available)"];
    }

    $pdo->prepare("UPDATE main_store_stock SET quantity = quantity - ? WHERE item_id = ?")->execute([$qty, $itemId]);
    consume_from_batches($pdo, $itemId, $qty);

    $pdo->prepare("INSERT INTO stock_transactions
        (item_id, location_type, location_id, txn_type, quantity, unit_cost, reference_type, reference_id, created_by, notes)
        VALUES (?, 'main_store', NULL, 'waste_out', ?, ?, 'WASTE', ?, ?, ?)")
        ->execute([$itemId, $qty, $unitCost, $refId, $userId, $notes]);

    return [true, 'OK'];
}

// Write off stock from an Outlet as waste
function outlet_waste_out(PDO $pdo, $itemId, $outletId, $qty, $unitCost, $refId, $userId, $notes = '') {
    $stmt = $pdo->prepare("SELECT quantity FROM outlet_stock WHERE outlet_id = ? AND item_id = ? FOR UPDATE");
    $stmt->execute([$outletId, $itemId]);
    $available = $stmt->fetchColumn() ?: 0;

    if ($available < $qty) {
        return [false, "Insufficient outlet stock to write off (available: $available)"];
    }

    $pdo->prepare("UPDATE outlet_stock SET quantity = quantity - ? WHERE outlet_id = ? AND item_id = ?")
        ->execute([$qty, $outletId, $itemId]);

    $pdo->prepare("INSERT INTO stock_transactions
        (item_id, location_type, location_id, txn_type, quantity, unit_cost, reference_type, reference_id, created_by, notes)
        VALUES (?, 'outlet', ?, 'waste_out', ?, ?, 'WASTE', ?, ?, ?)")
        ->execute([$itemId, $outletId, $qty, $unitCost, $refId, $userId, $notes]);

    return [true, 'OK'];
}

// ---------------------------------------------------------
// Accounting: General Ledger posting engine
// ---------------------------------------------------------

// Post a balanced double-entry journal entry.
// $lines = [ ['account_id'=>1,'debit'=>100,'credit'=>0,'description'=>'...'], ... ]
// Returns [true, entryId] on success or [false, errorMessage] if unbalanced.
function post_journal_entry(PDO $pdo, $entryDate, $description, array $lines, $userId, $refType = 'MANUAL', $refId = null) {
    $totalDebit = 0; $totalCredit = 0;
    foreach ($lines as $l) { $totalDebit += (float)$l['debit']; $totalCredit += (float)$l['credit']; }

    if (round($totalDebit, 2) !== round($totalCredit, 2)) {
        return [false, sprintf('Entry does not balance: debits ₦%s vs credits ₦%s', number_format($totalDebit,2), number_format($totalCredit,2))];
    }
    if ($totalDebit == 0) {
        return [false, 'Entry has no amounts.'];
    }

    $entryNumber = generate_doc_number($pdo, 'journal_entries', 'entry_number', 'JE');
    $pdo->prepare("INSERT INTO journal_entries (entry_number, entry_date, description, reference_type, reference_id, created_by)
                   VALUES (?,?,?,?,?,?)")
        ->execute([$entryNumber, $entryDate, $description, $refType, $refId, $userId]);
    $entryId = $pdo->lastInsertId();

    $ins = $pdo->prepare("INSERT INTO journal_entry_lines (entry_id, account_id, debit, credit, description) VALUES (?,?,?,?,?)");
    foreach ($lines as $l) {
        if ((float)$l['debit'] == 0 && (float)$l['credit'] == 0) continue;
        $ins->execute([$entryId, $l['account_id'], (float)$l['debit'], (float)$l['credit'], $l['description'] ?? null]);
    }

    return [true, $entryId];
}

// Void an entry by creating an equal-and-opposite reversing entry (never
// edits or deletes history - standard accounting practice)
function void_journal_entry(PDO $pdo, $entryId, $userId, $reason = '') {
    $stmt = $pdo->prepare("SELECT * FROM journal_entries WHERE id = ?");
    $stmt->execute([$entryId]);
    $entry = $stmt->fetch();
    if (!$entry || $entry['status'] === 'voided') return [false, 'Entry not found or already voided.'];

    $lineStmt = $pdo->prepare("SELECT * FROM journal_entry_lines WHERE entry_id = ?");
    $lineStmt->execute([$entryId]);
    $lines = $lineStmt->fetchAll();

    $reversedLines = array_map(fn($l) => [
        'account_id' => $l['account_id'], 'debit' => $l['credit'], 'credit' => $l['debit'],
        'description' => 'Reversal of ' . $entry['entry_number'] . ($reason ? ": $reason" : ''),
    ], $lines);

    [$ok, $result] = post_journal_entry($pdo, date('Y-m-d'), 'Void of ' . $entry['entry_number'] . ($reason ? ": $reason" : ''),
        $reversedLines, $userId, 'VOID', $entryId);
    if (!$ok) return [false, $result];

    $pdo->prepare("UPDATE journal_entries SET status='voided', voided_by_entry_id=? WHERE id=?")->execute([$result, $entryId]);
    return [true, $result];
}

// Net balance of an account from posted entries, optionally as-of a date
// and/or from a start date (for period activity). Returned in the account's
// natural direction (positive = normal balance side).
function get_account_balance(PDO $pdo, $accountId, $normalBalance, $asOfDate = null, $fromDate = null) {
    $sql = "SELECT COALESCE(SUM(jel.debit),0) d, COALESCE(SUM(jel.credit),0) c
            FROM journal_entry_lines jel
            JOIN journal_entries je ON je.id = jel.entry_id
            WHERE jel.account_id = ? AND je.status = 'posted'";
    $params = [$accountId];
    if ($asOfDate) { $sql .= " AND je.entry_date <= ?"; $params[] = $asOfDate; }
    if ($fromDate) { $sql .= " AND je.entry_date >= ?"; $params[] = $fromDate; }
    $stmt = $pdo->prepare($sql);
    $stmt->execute($params);
    $row = $stmt->fetch();

    return $normalBalance === 'debit' ? ($row['d'] - $row['c']) : ($row['c'] - $row['d']);
}

// The "control account" for a given account type name, e.g. the Trade
// Payables account used automatically by every AP bill. If more than one
// active account shares that type, the first (by code) is used.
function get_control_account(PDO $pdo, $typeName) {
    $stmt = $pdo->prepare("SELECT coa.* FROM chart_of_accounts coa
                            JOIN account_types at ON at.id = coa.account_type_id
                            WHERE at.name = ? AND coa.is_active = 1
                            ORDER BY coa.account_code LIMIT 1");
    $stmt->execute([$typeName]);
    return $stmt->fetch();
}

// All chart-of-accounts rows with their type info, for dropdowns/autocomplete
function get_chart_of_accounts(PDO $pdo, $activeOnly = true) {
    $sql = "SELECT coa.*, at.name type_name, at.classification, at.normal_balance, at.statement
            FROM chart_of_accounts coa JOIN account_types at ON at.id = coa.account_type_id";
    if ($activeOnly) $sql .= " WHERE coa.is_active = 1";
    $sql .= " ORDER BY coa.account_code";
    return $pdo->query($sql)->fetchAll();
}

// Manual stock adjustment (main store only) - positive or negative.
// Pass $expiryDate for a positive adjustment to also record a trackable batch.
function main_store_adjustment(PDO $pdo, $itemId, $qty, $userId, $notes = '', $expiryDate = null) {
    $type = $qty >= 0 ? 'adjustment_in' : 'adjustment_out';
    $absQty = abs($qty);

    $stmt = $pdo->prepare("SELECT * FROM main_store_stock WHERE item_id = ?");
    $stmt->execute([$itemId]);
    $row = $stmt->fetch();

    if ($row) {
        $newQty = $row['quantity'] + $qty;
        $pdo->prepare("UPDATE main_store_stock SET quantity = ? WHERE item_id = ?")->execute([$newQty, $itemId]);
    } else {
        $pdo->prepare("INSERT INTO main_store_stock (item_id, quantity, avg_cost) VALUES (?,?,0)")
            ->execute([$itemId, $qty]);
    }

    $pdo->prepare("INSERT INTO stock_transactions
        (item_id, location_type, location_id, txn_type, quantity, reference_type, created_by, notes)
        VALUES (?, 'main_store', NULL, ?, ?, 'ADJUSTMENT', ?, ?)")
        ->execute([$itemId, $type, $absQty, $userId, $notes]);

    if ($qty > 0 && $expiryDate) {
        $currentAvgCost = $row['avg_cost'] ?? 0;
        add_item_batch($pdo, $itemId, $qty, $currentAvgCost, $expiryDate, date('Y-m-d'));
    } elseif ($qty < 0) {
        consume_from_batches($pdo, $itemId, $absQty);
    }
}

// ---------------------------------------------------------
// AJAX helpers
// ---------------------------------------------------------

// "5m ago", "2h ago", "3d ago" style relative time for the notification dropdown
function time_ago($datetime) {
    $diff = time() - strtotime($datetime);
    if ($diff < 60) return 'just now';
    if ($diff < 3600) return floor($diff / 60) . 'm ago';
    if ($diff < 86400) return floor($diff / 3600) . 'h ago';
    if ($diff < 604800) return floor($diff / 86400) . 'd ago';
    return date('M j', strtotime($datetime));
}


// True if this request was made via fetch() with our standard AJAX header
// (set automatically by any <form class="ajax-form">, see toast_ajax.php)
function is_ajax() {
    return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
}

// Send a JSON response and stop execution - used by AJAX-aware form handlers.
// $extra can include e.g. ['redirect' => 'purchase_requisitions/view.php?id=5']
function json_response($success, $message, $extra = []) {
    if (ob_get_level()) ob_end_clean();
    header('Content-Type: application/json');
    echo json_encode(array_merge(['success' => $success, 'message' => $message], $extra));
    exit;
}

// ---------------------------------------------------------
// Double-submit protection
// ---------------------------------------------------------
// Generates a one-time form token to prevent double submission (double-click,
// back-button resubmit, a flaky connection silently retrying the POST, or a
// duplicate tab). Call once when rendering a form; call consume_form_token()
// when handling its POST - a second submission with the same token is
// rejected instead of silently creating a second record (and firing a
// second round of approval notifications).
function new_form_token($formName) {
    if (session_status() === PHP_SESSION_NONE && !headers_sent()) session_start();
    session_write_close();
    $token = bin2hex(random_bytes(16));
    $_SESSION['form_tokens'][$formName][$token] = time();
    foreach (($_SESSION['form_tokens'][$formName] ?? []) as $t => $ts) {
        if (time() - $ts > 3600) unset($_SESSION['form_tokens'][$formName][$t]);
    }
    return $token;
}

// Returns true exactly once per token - a second submission with the same
// token (double-click, resubmit) returns false so the caller can reject it.
function consume_form_token($formName, $token) {
    if (empty($token) || empty($_SESSION['form_tokens'][$formName][$token])) return false;
    unset($_SESSION['form_tokens'][$formName][$token]);
    return true;
}

