<?php
// ============================================================
//  MediCore — Stock Modifications API
//  File: api/stock-modifications.php
//
//  Manual stock increase/decrease with a mandatory reason, backing
//  Product List's "Stock Modification" button + tab. Every save here
//  writes THREE things atomically: the stock_modifications row itself
//  (permanent — no edit/delete endpoint exists for it, by design),
//  a stock_ledger entry (so it shows up in Product History's timeline
//  alongside sales/purchases/returns), and the usual real-time/cloud-
//  sync outbox writes every other stock-mutating endpoint makes.
// ============================================================

// ── CSV Export (GET ?action=export_csv) ──────────────────────
if (($_GET['action'] ?? '') === 'export_csv') {
    require_once '../includes/db.php';
    require_once '../includes/auth.php';
    require_login();
    session_write_close();
    if (!can('stock_modification', 'view')) { http_response_code(403); exit('Access denied.'); }

    [$wSql, $params] = build_filter_where($_GET);
    $rows = db_rows(
        "SELECT sm.created_at, p.name AS product_name, sm.modification_type,
                sm.quantity, sm.stock_before, sm.stock_after, sm.reason, sm.remarks,
                u.name AS user_name
           FROM stock_modifications sm
           JOIN products p ON p.id = sm.product_id
      LEFT JOIN users u ON u.id = sm.created_by
          WHERE $wSql
       ORDER BY sm.created_at DESC, sm.id DESC",
        $params
    );
    header('Content-Type: text/csv; charset=utf-8');
    header('Content-Disposition: attachment; filename="stock_modifications_' . date('Y-m-d') . '.csv"');
    $out = fopen('php://output', 'w');
    fputcsv($out, ['Date & Time', 'Product', 'Type', 'Quantity', 'Stock Before', 'Stock After', 'Reason', 'Remarks', 'User']);
    foreach ($rows as $r) {
        fputcsv($out, [
            $r['created_at'], $r['product_name'], ucfirst($r['modification_type']),
            $r['quantity'], $r['stock_before'], $r['stock_after'], $r['reason'], $r['remarks'], $r['user_name'] ?? '',
        ]);
    }
    fclose($out);
    exit;
}

require_once '../includes/db.php';
require_once '../includes/auth.php';
require_once '../includes/audit.php';
require_login();
session_write_close();

header('Content-Type: application/json');

// Must run before any transaction starts (DDL-inside-transaction rule
// — see the identical comment in api/sales.php etc.). This file was
// missing ensure_stock_events_table() specifically: api_save() below
// calls publish_stock_event() from inside db()->beginTransaction(), and
// publish_stock_event() lazily runs `CREATE TABLE IF NOT EXISTS
// stock_events` on its own first use per request. Running that DDL
// mid-transaction makes MySQL implicitly commit right there — the
// products.stock_pcs UPDATE and stock_ledger INSERT go through for
// real, but PDO still thinks a transaction is open, so the later
// db()->commit() throws "There is no active transaction" and the
// request reports failure even though the stock change already saved.
// Every other endpoint that calls publish_stock_event() in a
// transaction (sales.php, purchases.php, sale-returns.php,
// purchase-returns.php) already pre-creates this table up here; this
// file just missed it.
ensure_stock_events_table();
ensure_stock_ledger_table();
ensure_stock_ledger_opening_backfill();
ensure_sync_tables();
ensure_stock_modifications_table();

$body   = json_decode(file_get_contents('php://input'), true) ?? [];
$action = trim($body['action'] ?? '');

try {
    match ($action) {
        'save'     => api_save($body),
        'list'     => api_list($body),
        'users'    => api_users(),
        default    => fail('Unknown action')
    };
} catch (Throwable $e) {
    fail($e->getMessage());
}

function ensure_stock_modifications_table(): void {
    static $done = false;
    if ($done) return;
    $done = true;
    db_run("CREATE TABLE IF NOT EXISTS stock_modifications (
        id                 INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        product_id         INT UNSIGNED NOT NULL,
        modification_type  ENUM('increase','decrease') NOT NULL,
        quantity           INT UNSIGNED NOT NULL,
        stock_before       INT NOT NULL,
        stock_after        INT NOT NULL,
        reason             VARCHAR(150) NOT NULL,
        remarks            VARCHAR(255) DEFAULT '',
        created_by         INT UNSIGNED DEFAULT NULL,
        created_at         TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP(6),
        INDEX idx_product (product_id),
        INDEX idx_created (created_at)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
}

// ── Save a stock modification ───────────────────────────────────
function api_save(array $b): void {
    if (!can('stock_modification', 'create')) fail('You do not have permission to create stock modifications.');

    $productId = (int) ($b['product_id'] ?? 0);
    $type      = trim($b['modification_type'] ?? '');
    $qty       = (int) ($b['quantity'] ?? 0);
    $reason    = trim($b['reason'] ?? '');
    $remarks   = trim($b['remarks'] ?? '');

    if (!$productId) fail('Product is required.');
    if (!in_array($type, ['increase', 'decrease'], true)) fail('Modification type must be Increase or Decrease.');
    if ($qty <= 0) fail('Quantity must be greater than zero.');
    if ($reason === '') fail('Reason is required.');

    $product = db_row("SELECT id, name, stock_pcs FROM products WHERE id = ? AND is_deleted = 0", [$productId]);
    if (!$product) fail('Product not found.');
    $stockBefore = (int) $product['stock_pcs'];

    db()->beginTransaction();
    try {
        if ($type === 'increase') {
            db_run("UPDATE products SET stock_pcs = stock_pcs + ?, updated_at = NOW() WHERE id = ?", [$qty, $productId]);
        } else {
            // Atomic conditional decrement — the same oversell-race-safe
            // pattern used by every other stock-mutating endpoint in this
            // app (InnoDB's row lock is the actual enforcement point, not
            // just a pre-check): if two requests raced to decrease past
            // zero, only one UPDATE can match and affect a row.
            $affected = db_run(
                "UPDATE products SET stock_pcs = stock_pcs - ?, updated_at = NOW()
                  WHERE id = ? AND stock_pcs >= ?",
                [$qty, $productId, $qty]
            );
            if ($affected === 0) {
                db()->rollBack();
                $avail = (int) db_val("SELECT stock_pcs FROM products WHERE id = ?", [$productId]);
                fail("Cannot decrease stock below zero — only {$avail} pcs available, requested {$qty}.");
            }
        }

        $stockAfter = (int) db_val("SELECT stock_pcs FROM products WHERE id = ?", [$productId]);

        $modId = db_insert(
            "INSERT INTO stock_modifications
                (product_id, modification_type, quantity, stock_before, stock_after, reason, remarks, created_by)
             VALUES (?,?,?,?,?,?,?,?)",
            [$productId, $type, $qty, $stockBefore, $stockAfter, $reason, $remarks, current_user_id()]
        );

        $ledgerType = $type === 'increase' ? 'stock_increase' : 'stock_decrease';
        record_stock_ledger(
            $productId, $ledgerType,
            $type === 'increase' ? $qty : 0,
            $type === 'increase' ? 0 : $qty,
            $stockAfter, 'stock_modifications', $modId, '',
            $reason . ($remarks !== '' ? " — $remarks" : '')
        );

        publish_stock_event(
            $productId, $ledgerType,
            $type === 'increase' ? $qty : -$qty,
            $stockAfter, 'stock_modification', $modId
        );

        // Ready for the Cloud sync worker the moment it's deployed — see
        // SYNC_ARCHITECTURE.md, which explicitly documented this table as
        // "not built yet, wire in with the same one-line pattern when it
        // is." This is that line.
        enqueue_sync(
            'stock_modifications', $modId, 'INSERT',
            db_row("SELECT * FROM stock_modifications WHERE id = ?", [$modId])
        );

        db()->commit();

        log_action('create', 'stock_modifications', $modId,
            ucfirst($type) . " {$qty} pcs — {$product['name']} ({$stockBefore} → {$stockAfter}) — Reason: {$reason}");

        ok([
            'id'           => $modId,
            'stock_before' => $stockBefore,
            'stock_after'  => $stockAfter,
            'msg'          => 'Stock modification saved.',
        ]);
    } catch (Throwable $e) {
        db()->rollBack();
        fail('Save failed: ' . $e->getMessage());
    }
}

// ── Filterable, paginated log ─────────────────────────────────
function api_list(array $b): void {
    if (!can('stock_modification', 'view')) fail('You do not have permission to view the stock modification log.');

    [$wSql, $params] = build_filter_where($b);

    $page     = max(1, (int) ($b['page'] ?? 1));
    $pageSize = max(5, min(200, (int) ($b['page_size'] ?? 20)));
    $offset   = ($page - 1) * $pageSize;

    $total = (int) db_val(
        "SELECT COUNT(*) FROM stock_modifications sm JOIN products p ON p.id = sm.product_id WHERE $wSql",
        $params
    );

    $rows = db_rows(
        "SELECT sm.*, p.name AS product_name, u.name AS user_name
           FROM stock_modifications sm
           JOIN products p ON p.id = sm.product_id
      LEFT JOIN users u ON u.id = sm.created_by
          WHERE $wSql
       ORDER BY sm.created_at DESC, sm.id DESC
          LIMIT $pageSize OFFSET $offset",
        $params
    );

    ok([
        'data'        => $rows,
        'page'        => $page,
        'total'       => $total,
        'total_pages' => (int) max(1, ceil($total / $pageSize)),
    ]);
}

// ── Users for the filter dropdown ─────────────────────────────
function api_users(): void {
    ok(['data' => db_rows("SELECT id, name FROM users WHERE is_active = 1 ORDER BY name")]);
}

// Shared between api_list()/export_csv — kept as a plain function
// (not a closure) so the top-of-file export_csv branch, which runs
// before api_list() is even declared in file order, can still call it:
// function declarations are hoisted regardless of position, unlike
// top-level `const` (a distinction that bit this codebase once already
// — see api/products.php's THUMB_MAX_DIM history).
function build_filter_where(array $b): array {
    $where  = ['1=1'];
    $params = [];

    $q = trim($b['q'] ?? '');
    if ($q !== '') {
        $where[]  = "(p.name LIKE ? OR sm.reason LIKE ? OR sm.remarks LIKE ?)";
        $params[] = "%$q%"; $params[] = "%$q%"; $params[] = "%$q%";
    }
    $productId = (int) ($b['product_id'] ?? 0);
    if ($productId) { $where[] = "sm.product_id = ?"; $params[] = $productId; }

    $userId = (int) ($b['user_id'] ?? 0);
    if ($userId) { $where[] = "sm.created_by = ?"; $params[] = $userId; }

    $type = trim($b['modification_type'] ?? '');
    if (in_array($type, ['increase', 'decrease'], true)) { $where[] = "sm.modification_type = ?"; $params[] = $type; }

    $from = trim($b['from'] ?? '');
    if ($from !== '') { $where[] = "DATE(sm.created_at) >= ?"; $params[] = $from; }

    $to = trim($b['to'] ?? '');
    if ($to !== '') { $where[] = "DATE(sm.created_at) <= ?"; $params[] = $to; }

    return [implode(' AND ', $where), $params];
}

function ok(array $extra = []): void {
    echo json_encode(array_merge(['success' => true], $extra));
    exit;
}
function fail(string $msg): never {
    echo json_encode(['success' => false, 'error' => $msg]);
    exit;
}
