<?php
declare(strict_types=1);

require_once __DIR__ . '/../app/bootstrap.php';

$path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';

$basePath = rtrim(($config['app']['base_path'] ?? ''), '/');
if ($basePath !== '' && str_starts_with($path, $basePath)) {
    $path = substr($path, strlen($basePath));
}

$path = preg_replace('#^/index\.php#', '', $path);
$path = '/' . ltrim((string)$path, '/');
if ($path === '//') {
    $path = '/';
}

$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';

/**
 * Prefix an app route with base path
 */
function to(string $route): string {
    global $basePath;
    if ($basePath === '') return $route;
    if ($route === '/') return $basePath . '/';
    return $basePath . $route;
}

function render(string $view, array $data = []): void {
    extract($data);
    require __DIR__ . '/../app/views/' . $view . '.php';
}

function layout(string $contentView, array $data = []): void {
    global $auth, $config;

    $user = $auth->user();
    $appName = $config['app']['name'] ?? 'WSC Logistics';
    $siteKey = $config['recaptcha']['site_key'] ?? '';
    $logoUrl = $config['app']['logo_url'] ?? '/assets/logo-banner-placeholder.png';

    render('layout', compact('contentView', 'data', 'user', 'appName', 'siteKey', 'logoUrl'));
}

function partial(string $view, array $data = []): void {
    render($view, $data);
}

/*
|--------------------------------------------------------------------------
| Root/login handling
|--------------------------------------------------------------------------
*/
$isLoggedIn = !empty($_SESSION['uid']) || !empty($_SESSION['user_id']);

if ($method === 'GET' && $path === '/' && !$isLoggedIn) {
    header('Location: ' . to('/login'));
    exit;
}

if ($method === 'GET' && $path === '/login' && $isLoggedIn) {
    header('Location: ' . to('/shipments'));
    exit;
}

$routes = [
    'GET' => [
        '/' => fn() => (function() {
            global $auth, $config;
        
            $auth->requireLogin();
        
            $bp = rtrim(($config['app']['base_path'] ?? ''), '/');
        
            header('Location: ' . ($bp ? $bp : '') . '/shipments');
            exit;
        })(),
        
        '/dashboard' => fn() => (function() {
            global $auth, $config;
        
            $auth->requireLogin();
        
            $bp = rtrim(($config['app']['base_path'] ?? ''), '/');
        
            header('Location: ' . ($bp ? $bp : '') . '/shipments');
            exit;
        })(),

        '/login' => fn() => layout('pages/login'),

        '/logout' => fn() => (function() {
            global $auth;
            $auth->logout();
            header('Location: ' . to('/login'));
            exit;
        })(),
        '/shipments' => fn() => layout('pages/shipments_list'),
        '/shipments/view' => fn() => layout('pages/shipment_view'),
        '/hbl/drawer' => fn() => layout('pages/hbl_drawer'),
        '/mbl' => fn() => (function() { global $auth; $auth->requireLogin(); layout('pages/mbl_list'); })(),
        '/mbl/new' => fn() => (function() { global $auth; $auth->requireLogin(); layout('pages/mbl_new'); })(),
        '/hbl/new' => fn() => layout('pages/hbl_new'),
        '/hbl' => fn() => (function() { global $auth; $auth->requireLogin(); layout('pages/hbl_list'); })(),
        '/mbl/edit' => fn() => (function() { global $auth; $auth->requireLogin(); layout('pages/mbl_edit'); })(),
        '/hbl/edit' => fn() => (function() { global $auth; $auth->requireLogin(); layout('pages/hbl_edit'); })(),
        '/invoice/view' => fn() => (function() {
            global $auth;
            $auth->requireLogin();
            require __DIR__ . '/../app/views/pages/invoice_view.php';
            exit;
        })(),
        '/invoice/download' => fn() => (function() {
                global $auth;
                $auth->requireLogin();
                require __DIR__ . '/../app/views/pages/invoice_download.php';
            })(),
        '/invoice/select-charges' => fn() => (function() { global $auth; $auth->requireLogin(); layout('pages/invoice_select_charges'); })(),
        '/doc/arrival-notice' => fn() => (function() {
                global $auth;
                $auth->requireLogin();
                require __DIR__ . '/../app/views/pages/doc_arrival_notice.php';
                exit;
            })(),
        '/doc/receipt' => fn() => (function() {
                global $auth;
                $auth->requireLogin();
                require __DIR__ . '/../app/views/pages/doc_receipt.php';
                exit;
            })(),
        '/receipt/download' => fn() => (function() {
                global $auth;
                $auth->requireLogin();
                require __DIR__ . '/../app/views/pages/receipt_download.php';
            })(),
        '/doc/delivery-order' => fn() => (function() {
                global $auth;
                $auth->requireLogin();
                require __DIR__ . '/../app/views/pages/doc_delivery_order.php';
                exit;
            })(),
        '/doc/delivery-order/download' => fn() => (function() {
                global $auth;
                $auth->requireLogin();
                require __DIR__ . '/../app/views/pages/doc_delivery_order_download.php';
            })(),
        '/doc/manifest' => fn() => (function() {
                global $auth;
                $auth->requireLogin();
                require __DIR__ . '/../app/views/pages/doc_manifest.php';
                exit;
            })(),
        '/doc/manifest/download' => fn() => (function() {
    global $auth;
    $auth->requireLogin();

    require __DIR__ . '/../app/views/pages/doc_manifest_download.php';
    exit;
})(),
        '/customers' => fn() => (function() { global $auth; $auth->requireLogin(); layout('pages/customers_list'); })(),
        '/customers/new' => fn() => (function() { global $auth; $auth->requireLogin(); layout('pages/customers_new'); })(),
        '/customers/edit' => fn() => (function() { global $auth; $auth->requireLogin(); layout('pages/customers_edit'); })(),
        '/users' => fn() => (function() { global $auth; $auth->requireLogin(); layout('pages/users_list'); })(),
        '/users/new' => fn() => (function() { global $auth; $auth->requireLogin(); layout('pages/users_new'); })(),
        '/users/edit' => fn() => (function() { global $auth; $auth->requireLogin(); layout('pages/users_edit'); })(),
        '/change-password' => fn() => (function() { global $auth; $auth->requireLogin(); layout('pages/change_password'); })(),

        '/api/search/shipping-lines' => fn() => (function() {
            global $db, $auth;
            $auth->requireLogin();

            $q = trim((string)($_GET['q'] ?? ''));
            $qLike = '%' . $q . '%';

            $stmt = $db->prepare("SELECT id, name
                                  FROM shipping_lines
                                  WHERE is_active=1 AND name LIKE ?
                                  ORDER BY name
                                  LIMIT 30");
            $stmt->execute([$qLike]);

            $results = array_map(fn($r) => ['id' => $r['id'], 'text' => $r['name']], $stmt->fetchAll());
            json_out(['results' => $results]);
        })(),

        '/shipping-lines' => fn() => (function() { global $auth; $auth->requireLogin(); layout('pages/shipping_lines_list'); })(),

        '/api/invoices/balance' => fn() => (function() {
            global $db;

            header('Content-Type: application/json; charset=utf-8');

            try {
                $houseId = (int)($_GET['house_bl_id'] ?? 0);
                if ($houseId <= 0) throw new Exception('Invalid house_bl_id');

                $s = $db->prepare("
                    SELECT id, total
                    FROM invoices
                    WHERE house_bl_id=?
                    ORDER BY id DESC
                    LIMIT 1
                ");
                $s->execute([$houseId]);
                $inv = $s->fetch();

                if (!$inv) throw new Exception('No invoice found for this HBL');

                $invoiceId    = (int)$inv['id'];
                $invoiceTotal = (float)($inv['total'] ?? 0);

                $p = $db->prepare("SELECT COALESCE(SUM(amount),0) AS paid FROM payments WHERE invoice_id=?");
                $p->execute([$invoiceId]);
                $paid = (float)($p->fetch()['paid'] ?? 0);

                $balance = $invoiceTotal - $paid;
                if ($balance < 0) $balance = 0;

                echo json_encode([
                    'success'       => 1,
                    'invoice_id'    => $invoiceId,
                    'invoice_total' => round($invoiceTotal, 2),
                    'paid_total'    => round($paid, 2),
                    'balance'       => round($balance, 2),
                ]);
            } catch (Throwable $e) {
                http_response_code(400);
                echo json_encode([
                    'success' => 0,
                    'message' => $e->getMessage()
                ]);
            }
        })(),

        '/api/payments/latest' => fn() => (function() {
            global $db, $auth;
            $auth->requireLogin();

            header('Content-Type: application/json; charset=utf-8');

            try {
                $houseId = (int)($_GET['house_bl_id'] ?? 0);
                if ($houseId <= 0) throw new Exception('Invalid house_bl_id');

                $stmt = $db->prepare("
                    SELECT
                      p.id AS payment_id,
                      i.id AS invoice_id
                    FROM house_bls h
                    JOIN invoices i ON i.house_bl_id = h.id
                    JOIN payments p ON p.invoice_id = i.id
                    WHERE h.id = ?
                    ORDER BY p.id DESC
                    LIMIT 1
                ");
                $stmt->execute([$houseId]);
                $row = $stmt->fetch();

                echo json_encode([
                    'success'    => 1,
                    'found'      => $row ? 1 : 0,
                    'payment_id' => $row ? (int)$row['payment_id'] : 0,
                    'invoice_id' => $row ? (int)$row['invoice_id'] : 0,
                ]);
            } catch (Throwable $e) {
                http_response_code(400);
                echo json_encode([
                    'success' => 0,
                    'message' => $e->getMessage()
                ]);
            }
        })(),

        '/api/search/ports' => fn() => (function() {
            global $db, $auth;
            $auth->requireLogin();

            $q = trim((string)($_GET['q'] ?? ''));
            $qLike = '%' . $q . '%';

            $stmt = $db->prepare("SELECT id, name, country, unlocode
                                  FROM ports
                                  WHERE (name LIKE ? OR country LIKE ? OR unlocode LIKE ?)
                                  ORDER BY name
                                  LIMIT 30");
            $stmt->execute([$qLike, $qLike, $qLike]);

            $results = [];
            foreach ($stmt->fetchAll() as $r) {
                $label = $r['name'];
                if (!empty($r['country'])) $label .= " (" . $r['country'] . ")";
                if (!empty($r['unlocode'])) $label .= " — " . $r['unlocode'];
                $results[] = ['id' => $r['id'], 'text' => $label];
            }

            json_out(['results' => $results]);
        })(),

        '/api/search/customers' => fn() => (function() {
            global $db, $auth;
            $auth->requireLogin();

            $q = trim((string)($_GET['q'] ?? ''));
            $qLike = '%' . $q . '%';

            $stmt = $db->prepare("SELECT id, name, contact_person, phone, email
                                  FROM customers
                                  WHERE (name LIKE ? OR contact_person LIKE ? OR phone LIKE ? OR email LIKE ?)
                                  ORDER BY name
                                  LIMIT 30");
            $stmt->execute([$qLike, $qLike, $qLike, $qLike]);

            $results = [];
            foreach ($stmt->fetchAll() as $r) {
                $label = $r['name'];
                if (!empty($r['contact_person'])) $label .= " — " . $r['contact_person'];
                if (!empty($r['phone'])) $label .= " (" . $r['phone'] . ")";
                $results[] = ['id' => (int)$r['id'], 'text' => $label];
            }

            json_out(['results' => $results]);
        })(),

        '/api/search/mbls' => fn() => (function() {
            global $db, $auth;
            $auth->requireLogin();

            $q = trim((string)($_GET['q'] ?? ''));
            $like = '%' . $q . '%';

            $stmt = $db->prepare("
                SELECT m.id, m.mbl_no, m.vessel_name, m.voyage_no
                FROM master_bls m
                WHERE m.mbl_no LIKE ?
                   OR m.vessel_name LIKE ?
                   OR m.voyage_no LIKE ?
                ORDER BY m.id DESC
                LIMIT 30
            ");
            $stmt->execute([$like, $like, $like]);

            $results = [];
            foreach ($stmt->fetchAll() as $r) {
                $label = $r['mbl_no'];
                if ($r['vessel_name']) $label .= ' — ' . $r['vessel_name'];
                if ($r['voyage_no']) $label .= ' / ' . $r['voyage_no'];
                $results[] = ['id' => (int)$r['id'], 'text' => $label];
            }

            json_out(['results' => $results]);
        })(),

        // keep the rest of your existing GET routes here unchanged
    ],

    'POST' => [
        '/api/login' => fn() => (function() {
            global $auth, $config;
            require_post();

            $token = $_POST['g-recaptcha-response'] ?? '';
            $okCaptcha = recaptcha_verify($config['recaptcha']['secret_key'] ?? '', $token, $_SERVER['REMOTE_ADDR'] ?? '');
            if (!$okCaptcha) json_out(['ok' => false, 'msg' => 'reCAPTCHA failed.'], 400);

            $email = trim((string)($_POST['email'] ?? ''));
            $pass = (string)($_POST['password'] ?? '');
            $r = $auth->login($email, $pass);

            if (!$r['ok']) json_out(['ok' => false, 'msg' => $r['msg']], 401);

            json_out(['ok' => true, 'redirect' => to('/shipments')]);
        })(),
    
    '/api/shipping-lines/create' => fn() => (function() {
      global $db;
      require_post();

      $name = trim((string)($_POST['name'] ?? ''));
      if ($name === '') json_out(['ok'=>false,'msg'=>'Shipping line name is required'], 400);

      $chk = $db->prepare("SELECT id FROM shipping_lines WHERE name=? LIMIT 1");
      $chk->execute([$name]);
      if ($chk->fetch()) json_out(['ok'=>false,'msg'=>'Shipping line already exists'], 409);

      $stmt = $db->prepare("INSERT INTO shipping_lines(name, is_active) VALUES(?, 1)");
      $stmt->execute([$name]);

      json_out(['ok'=>true,'redirect'=>to('/shipping-lines')]);
    })(),

    '/api/mbl/create' => fn() => (function() {
      global $db, $auth;
      $auth->requireLogin();
      require_post();
    
      $mblNo = trim((string)($_POST['mbl_no'] ?? ''));
      $shippingLineId = (int)($_POST['shipping_line_id'] ?? 0);
      $polPortId = (int)($_POST['pol_port_id'] ?? 0);
      $podPortId = (int)($_POST['pod_port_id'] ?? 0);
      $etaDate = trim((string)($_POST['eta_date'] ?? ''));
      $vesselName = trim((string)($_POST['vessel_name'] ?? ''));
      $voyageNo = trim((string)($_POST['voyage_no'] ?? ''));
      $containerNo = trim((string)($_POST['container_no'] ?? ''));
      $sealNo = trim((string)($_POST['seal_no'] ?? ''));
    
      if ($mblNo === '') {
        json_out(['ok' => false, 'msg' => 'MBL Number is required'], 400);
      }
      if ($shippingLineId <= 0) {
        json_out(['ok' => false, 'msg' => 'Shipping Line is required'], 400);
      }
      if ($polPortId <= 0) {
        json_out(['ok' => false, 'msg' => 'Port of Loading is required'], 400);
      }
      if ($podPortId <= 0) {
        json_out(['ok' => false, 'msg' => 'Port of Destination is required'], 400);
      }
    
      $etaDate = $etaDate === '' ? null : $etaDate;
      $vesselName = $vesselName === '' ? null : $vesselName;
      $voyageNo = $voyageNo === '' ? null : $voyageNo;
      $containerNo = $containerNo === '' ? null : $containerNo;
      $containerType = trim($_POST['container_type'] ?? '');
      $sealNo = $sealNo === '' ? null : $sealNo;
    
      $dup = $db->prepare("SELECT id FROM master_bls WHERE mbl_no = ? LIMIT 1");
      $dup->execute([$mblNo]);
      if ($dup->fetch()) {
        json_out(['ok' => false, 'msg' => 'That MBL Number already exists'], 409);
      }
    
      $createdBy = (int)($_SESSION['uid'] ?? $_SESSION['user_id'] ?? 0);
      if ($createdBy <= 0) {
        json_out(['ok' => false, 'msg' => 'Session user not found'], 401);
      }
    
      $stmt = $db->prepare("
        INSERT INTO master_bls (
          mbl_no,
          shipping_line_id,
          pol_port_id,
          pod_port_id,
          vessel_name,
          voyage_no,
          container_no,
          eta_date,
          status,
          created_by,
          container_type,
          seal_no
        )
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'RECEIVED', ?, ?, ?)
      ");
    
      $stmt->execute([
        $mblNo,
        $shippingLineId,
        $polPortId,
        $podPortId,
        $vesselName,
        $voyageNo,
        $containerNo,
        $etaDate,
        $createdBy,
        $containerType !== '' ? $containerType : null,
        $sealNo
      ]);
    
      json_out([
        'ok' => true,
        'redirect' => to('/mbl')
      ]);
    })(),

    '/api/hbl/create' => fn() => (function() {
  global $db;
  require_post();

  $mbl = (int)($_POST['master_bl_id'] ?? 0);
  $hbl_no = trim((string)($_POST['hbl_no'] ?? ''));
  $consignee = (int)($_POST['consignee_customer_id'] ?? 0);
  $billing = (int)($_POST['billing_party_customer_id'] ?? 0);
  $notify = (int)($_POST['notify_party_customer_id'] ?? 0);

  $grossKg = (float)($_POST['gross_weight_kg'] ?? 0);
  $cbm = (float)($_POST['cbm'] ?? 0);

  $chargeableCbm = (int)ceil($cbm);
  $chargeableWeightTon = round($grossKg / 1000.0, 3);
  $chargeableUnits = max($chargeableWeightTon, (float)$chargeableCbm);

  $manifestNo   = trim((string)($_POST['manifest_no'] ?? ''));
  $hsCode       = trim((string)($_POST['hs_code'] ?? ''));
  $icmsBlNo     = trim((string)($_POST['icms_bl_no'] ?? ''));
  $cfs          = trim((string)($_POST['cfs'] ?? ''));
  $marksNumbers = trim((string)($_POST['marks_numbers'] ?? ''));

  $manifestNo   = $manifestNo === '' ? null : $manifestNo;
  $hsCode       = $hsCode === '' ? null : $hsCode;
  $icmsBlNo     = $icmsBlNo === '' ? null : $icmsBlNo;
  $cfs          = $cfs === '' ? null : $cfs;
  $marksNumbers = $marksNumbers === '' ? null : $marksNumbers;

  $stmt = $db->prepare("
    INSERT INTO house_bls(
      master_bl_id,hbl_no,consignee_customer_id,billing_party_customer_id,notify_party_customer_id,
      goods_description,packages,gross_weight_kg,cbm,chargeable_cbm,chargeable_weight_ton,chargeable_units,
      manifest_no,hs_code,icms_bl_no,cfs,marks_numbers
    )
    VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
  ");

  $stmt->execute([
    $mbl,
    $hbl_no,
    $consignee,
    $billing,
    ($notify ?: null),
    ($_POST['goods_description'] ?? '') ?: null,
    (int)($_POST['packages'] ?? 0),
    $grossKg,
    $cbm,
    $chargeableCbm,
    $chargeableWeightTon,
    $chargeableUnits,
    $manifestNo,
    $hsCode,
    $icmsBlNo,
    $cfs,
    $marksNumbers
  ]);

  json_out(['ok'=>true]);
})(),

    '/api/mbl/update' => fn() => (function() {
  global $db, $auth;
  $auth->requireLogin();
  require_post();

  $id = (int)($_POST['id'] ?? 0);
  if ($id <= 0) json_out(['ok'=>false,'msg'=>'Invalid MBL'], 400);

  $mblNo = trim((string)($_POST['mbl_no'] ?? ''));
  if ($mblNo === '') json_out(['ok'=>false,'msg'=>'MBL Number is required'], 400);

  $shippingLineId = (int)($_POST['shipping_line_id'] ?? 0);
  $polPortId = (int)($_POST['pol_port_id'] ?? 0);
  $podPortId = (int)($_POST['pod_port_id'] ?? 0);

  if ($shippingLineId <= 0) json_out(['ok'=>false,'msg'=>'Shipping Line is required'], 400);
  if ($polPortId <= 0) json_out(['ok'=>false,'msg'=>'Port of Loading is required'], 400);
  if ($podPortId <= 0) json_out(['ok'=>false,'msg'=>'Port of Destination is required'], 400);

  $etaDate = trim((string)($_POST['eta_date'] ?? ''));
  $vesselName = trim((string)($_POST['vessel_name'] ?? ''));
  $voyageNo = trim((string)($_POST['voyage_no'] ?? ''));
  $containerNo = trim((string)($_POST['container_no'] ?? ''));
  $sealNo = trim((string)($_POST['seal_no'] ?? ''));

  $etaDate = $etaDate === '' ? null : $etaDate;
  $vesselName = $vesselName === '' ? null : $vesselName;
  $voyageNo = $voyageNo === '' ? null : $voyageNo;
  $containerNo = $containerNo === '' ? null : $containerNo;
  $containerType = trim($_POST['container_type'] ?? '');
  $sealNo = $sealNo === '' ? null : $sealNo;

  $dup = $db->prepare("SELECT id FROM master_bls WHERE mbl_no = ? AND id <> ? LIMIT 1");
  $dup->execute([$mblNo, $id]);
  if ($dup->fetch()) {
    json_out(['ok'=>false,'msg'=>'Another MBL already uses that number'], 409);
  }

  $stmt = $db->prepare("
    UPDATE master_bls
    SET mbl_no=?,
        shipping_line_id=?,
        pol_port_id=?,
        pod_port_id=?,
        vessel_name=?,
        voyage_no=?,
        eta_date=?,
        container_no=?,
        container_type = ?,
        seal_no=?,
        updated_at=NOW()
    WHERE id=?
  ");

  $stmt->execute([
    $mblNo,
    $shippingLineId,
    $polPortId,
    $podPortId,
    $vesselName,
    $voyageNo,
    $etaDate,
    $containerNo,
    $containerType !== '' ? $containerType : null,
    $sealNo,
    $id
  ]);

  json_out(['ok'=>true,'redirect'=>to('/mbl')]);
})(),

    '/api/hbl/update' => fn() => (function() {
  global $db;
  require_post();

  $id = (int)($_POST['id'] ?? 0);
  if ($id <= 0) json_out(['ok'=>false,'msg'=>'Invalid HBL'], 400);

  $mbl = (int)($_POST['master_bl_id'] ?? 0);
  if ($mbl <= 0) json_out(['ok'=>false,'msg'=>'MBL is required'], 400);

  $hblNo = trim((string)($_POST['hbl_no'] ?? ''));
  if ($hblNo === '') json_out(['ok'=>false,'msg'=>'HBL No is required'], 400);

  $consignee = (int)($_POST['consignee_customer_id'] ?? 0);
  $billing  = (int)($_POST['billing_party_customer_id'] ?? 0);
  if ($consignee <= 0 || $billing <= 0) json_out(['ok'=>false,'msg'=>'Consignee and Billing Party are required'], 400);

  $notify = (int)($_POST['notify_party_customer_id'] ?? 0);
  $grossKg = (float)($_POST['gross_weight_kg'] ?? 0);
  $cbm     = (float)($_POST['cbm'] ?? 0);

  $chargeableCbm = (int)ceil($cbm);
  $chargeableWeightTon = round($grossKg / 1000.0, 3);
  $chargeableUnits = max($chargeableWeightTon, (float)$chargeableCbm);

  $manifestNo   = trim((string)($_POST['manifest_no'] ?? ''));
  $hsCode       = trim((string)($_POST['hs_code'] ?? ''));
  $icmsBlNo     = trim((string)($_POST['icms_bl_no'] ?? ''));
  $cfs          = trim((string)($_POST['cfs'] ?? ''));
  $marksNumbers = trim((string)($_POST['marks_numbers'] ?? ''));

  $manifestNo   = $manifestNo === '' ? null : $manifestNo;
  $hsCode       = $hsCode === '' ? null : $hsCode;
  $icmsBlNo     = $icmsBlNo === '' ? null : $icmsBlNo;
  $cfs          = $cfs === '' ? null : $cfs;
  $marksNumbers = $marksNumbers === '' ? null : $marksNumbers;

  $stmt = $db->prepare("UPDATE house_bls
                        SET master_bl_id=?,
                            hbl_no=?,
                            consignee_customer_id=?,
                            billing_party_customer_id=?,
                            notify_party_customer_id=?,
                            goods_description=?,
                            packages=?,
                            gross_weight_kg=?,
                            cbm=?,
                            chargeable_cbm=?,
                            chargeable_weight_ton=?,
                            chargeable_units=?,
                            manifest_no = ?,
                            hs_code = ?,
                            icms_bl_no = ?,
                            cfs = ?,
                            marks_numbers = ?
                        WHERE id=?");
  $stmt->execute([
    $mbl,
    $hblNo,
    $consignee,
    $billing,
    ($notify ?: null),
    ($_POST['goods_description'] ?? '') ?: null,
    (int)($_POST['packages'] ?? 0),
    $grossKg,
    $cbm,
    $chargeableCbm,
    $chargeableWeightTon,
    $chargeableUnits,
    $manifestNo,
    $hsCode,
    $icmsBlNo,
    $cfs,
    $marksNumbers,
    $id
  ]);

  $back = '/hbl' . ($mbl ? ('?master_bl_id=' . $mbl) : '');
  json_out(['ok'=>true,'redirect'=>to($back)]);
})(),

    '/api/mbl/status' => fn() => (function() {
      global $db, $auth;
      $auth->requireLogin();
      require_post();

      $id = (int)($_POST['id'] ?? 0);
      $status = strtoupper(trim((string)($_POST['status'] ?? '')));
      $allowed = ['RECEIVED','IN_PROGRESS','RELEASED'];

      if ($id<=0) json_out(['ok'=>false,'msg'=>'Invalid MBL id'], 400);
      if (!in_array($status, $allowed, true)) json_out(['ok'=>false,'msg'=>'Invalid status'], 400);

      $db->prepare("UPDATE master_bls SET status=?, updated_at=NOW() WHERE id=?")->execute([$status, $id]);
      json_out(['ok'=>true,'message'=>'MBL updated.']);
    })(),

    '/api/hbl/status' => fn() => (function() {
      global $db, $auth;
      $auth->requireLogin();
      require_post();

      $id = (int)($_POST['id'] ?? 0);
      $status = strtoupper(trim((string)($_POST['status'] ?? '')));

      $allowed = ['PENDING','AWAITING_INVOICE','AWAITING_PAYMENT','READY_FOR_DO','DO_ISSUED','RELEASED'];
      if ($id<=0) json_out(['ok'=>false,'msg'=>'Invalid HBL id'], 400);
      if (!in_array($status, $allowed, true)) json_out(['ok'=>false,'msg'=>'Invalid status'], 400);

      $db->prepare("UPDATE house_bls SET status=?, updated_at=NOW() WHERE id=?")->execute([$status, $id]);
      json_out(['ok'=>true,'message'=>'HBL updated.']);
    })(),
    
    '/api/invoice/mark-issued' => fn() => (function() {
    global $db, $auth;
    $auth->requireLogin();
    require_post();

    $invoiceId = (int)($_POST['invoice_id'] ?? 0);
    if ($invoiceId <= 0) json_out(['ok'=>false,'msg'=>'Invalid invoice_id'], 400);

    // Read current status
    $st = $db->prepare("SELECT id, status FROM invoices WHERE id=? LIMIT 1");
    $st->execute([$invoiceId]);
    $inv = $st->fetch();
    if (!$inv) json_out(['ok'=>false,'msg'=>'Invoice not found'], 404);

    // Only transition DRAFT -> ISSUED, idempotent
    if ($inv['status'] !== 'ISSUED') {
      $db->beginTransaction();

      $db->prepare("UPDATE invoices SET status='ISSUED' WHERE id=?")->execute([$invoiceId]);

      // optional: log event (keeps your audit trail consistent)
      try {
        $db->prepare("
          INSERT INTO invoice_events(invoice_id, event_type, old_status, new_status, user_id, ip_address, user_agent, meta)
          VALUES(?, 'ISSUED', ?, 'ISSUED', ?, ?, ?, ?)
        ")->execute([
          $invoiceId,
          $inv['status'],
          $_SESSION['user_id'] ?? null,
          $_SERVER['REMOTE_ADDR'] ?? null,
          $_SERVER['HTTP_USER_AGENT'] ?? null,
          json_encode(['trigger' => 'print'])
        ]);
      } catch (Throwable $e) {
        // ignore if invoice_events table not present or schema differs
      }

      $db->commit();
    }

    json_out(['ok'=>true]);
  })(),

  
 '/api/payments/create' => fn() => (function() {
  global $db, $auth;
  $auth->requireLogin();

  header('Content-Type: application/json; charset=utf-8');

  if (!function_exists('wsc_vessel_initials')) {
    function wsc_vessel_initials(string $name): string {
      $name = strtoupper(trim($name));
      if ($name === '') return 'XX';

      $name = preg_replace('/[^A-Z0-9 ]+/', ' ', $name);
      $parts = preg_split('/\s+/', $name, -1, PREG_SPLIT_NO_EMPTY);

      if (!$parts) return 'XX';

      $initials = '';
      foreach ($parts as $p) {
        $initials .= substr($p, 0, 1);
      }

      return $initials ?: 'XX';
    }
  }

  if (!function_exists('wsc_manifest_position_for_house')) {
    function wsc_manifest_position_for_house(PDO $db, int $masterBlId, int $houseId): int {
      if ($masterBlId <= 0 || $houseId <= 0) return 1;

      $stmt = $db->prepare("
        SELECT COUNT(*) AS pos
        FROM house_bls
        WHERE master_bl_id = ?
          AND id <= ?
      ");
      $stmt->execute([$masterBlId, $houseId]);

      $pos = (int)($stmt->fetchColumn() ?: 1);
      return max(1, $pos);
    }
  }

  if (!function_exists('wsc_generate_receipt_no')) {
    function wsc_generate_receipt_no(PDO $db, int $houseId, string $payDate): string {
      $stmt = $db->prepare("
        SELECT
          h.id,
          h.master_bl_id,
          m.vessel_name,
          m.voyage_no
        FROM house_bls h
        JOIN master_bls m ON m.id = h.master_bl_id
        WHERE h.id = ?
        LIMIT 1
      ");
      $stmt->execute([$houseId]);
      $row = $stmt->fetch(PDO::FETCH_ASSOC);

      if (!$row) {
        throw new RuntimeException('Could not resolve shipment details for receipt number');
      }

      $initials   = wsc_vessel_initials((string)($row['vessel_name'] ?? ''));
      $voyageNo   = trim((string)($row['voyage_no'] ?? ''));
      $middle     = $initials . $voyageNo;

      if ($middle === '') {
        $middle = 'XX000';
      }

      try {
        $dt = new DateTime($payDate !== '' ? $payDate : 'now');
      } catch (Throwable $e) {
        $dt = new DateTime();
      }

      $timePart  = $dt->format('Hi');
      $monthPart = $dt->format('m');
      $yearPart  = $dt->format('y');

      $position = wsc_manifest_position_for_house(
        $db,
        (int)($row['master_bl_id'] ?? 0),
        (int)($row['id'] ?? 0)
      );

      $posPart = str_pad((string)$position, 2, '0', STR_PAD_LEFT);

      $receiptNo = 'WSCKE-' . $middle . '/' . $timePart . '/' . $monthPart . '/' . $yearPart . '/' . $posPart . 'RCPT';

      // collision check
      $chk = $db->prepare("SELECT id FROM payments WHERE receipt_no = ? LIMIT 1");
      $chk->execute([$receiptNo]);
      if ($chk->fetch()) {
        throw new RuntimeException('Duplicate receipt number generated. Retry payment save.');
      }

      return $receiptNo;
    }
  }

  try {
    $houseId  = (int)($_POST['house_bl_id'] ?? 0);
    $invoiceIdPost = (int)($_POST['invoice_id'] ?? 0);

    $method   = strtoupper(trim((string)($_POST['method'] ?? '')));
    $currency = strtoupper(trim((string)($_POST['currency'] ?? 'USD')));
    $refNo    = trim((string)($_POST['ref_no'] ?? ''));
    $payDate  = trim((string)($_POST['pay_date'] ?? ''));

    $amountCcy = (float)($_POST['amount'] ?? 0);
    $fxRate    = (float)($_POST['fx_rate'] ?? 0);

    if ($houseId <= 0) throw new Exception('Invalid house_bl_id');
    if (!in_array($method, ['CASH','BANK','MPESA','CHEQUE','OTHER'], true)) {
      throw new Exception('Invalid payment method');
    }
    if (!in_array($currency, ['USD','KES'], true)) {
      throw new Exception('Invalid currency');
    }
    if ($amountCcy <= 0) throw new Exception('Amount must be greater than 0');
    if ($payDate === '') throw new Exception('Payment date is required');

    if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $payDate)) {
      throw new Exception('Payment date must be in yyyy-mm-dd format');
    }

    // Get latest invoice for this HBL
    $s = $db->prepare("SELECT id, total FROM invoices WHERE house_bl_id=? ORDER BY id DESC LIMIT 1");
    $s->execute([$houseId]);
    $inv = $s->fetch();

    if (!$inv) throw new Exception('No invoice found for this HBL');

    $invoiceId = (int)$inv['id'];
    $invoiceTotalUsd = (float)($inv['total'] ?? 0);

    if ($invoiceIdPost > 0 && $invoiceIdPost !== $invoiceId) {
      throw new Exception('Invoice mismatch for this HBL');
    }

    if ($currency === 'USD') {
      $amountUsd = round($amountCcy, 2);
      $fxRate = null;
    } else {
      if ($fxRate <= 0) {
        throw new Exception('FX rate is required for KES payments');
      }
      $amountUsd = round($amountCcy / $fxRate, 2);
      $fxRate = round($fxRate, 6);
    }

    $p = $db->prepare("SELECT COALESCE(SUM(amount_usd),0) AS paid_usd FROM payments WHERE invoice_id=?");
    $p->execute([$invoiceId]);
    $paidUsd = (float)($p->fetch()['paid_usd'] ?? 0);

    $balanceUsd = round($invoiceTotalUsd - $paidUsd, 2);
    if ($balanceUsd < 0) $balanceUsd = 0;

    if ($amountUsd > $balanceUsd + 0.009) {
      throw new Exception('Amount exceeds invoice balance (Balance USD: ' . number_format($balanceUsd, 2) . ').');
    }

    $receiptNo = wsc_generate_receipt_no($db, $houseId, $payDate);

    $db->beginTransaction();

    try {
      $ins = $db->prepare("
        INSERT INTO payments (
          invoice_id,
          receipt_no,
          pay_date,
          method,
          ref_no,
          amount,
          currency,
          fx_rate,
          amount_ccy,
          amount_usd
        )
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
      ");
      $ins->execute([
        $invoiceId,
        $receiptNo,
        $payDate,
        $method,
        ($refNo !== '' ? $refNo : null),
        $amountCcy,
        $currency,
        $fxRate,
        $amountCcy,
        $amountUsd
      ]);

      $paymentId = (int)$db->lastInsertId();

      $newPaidUsd = round($paidUsd + $amountUsd, 2);
      if ($newPaidUsd >= $invoiceTotalUsd - 0.009) {
        $db->prepare("UPDATE invoices SET status='PAID' WHERE id=?")->execute([$invoiceId]);

        $driver = $db->getAttribute(PDO::ATTR_DRIVER_NAME);
        $nowExpr = ($driver === 'sqlsrv') ? 'GETDATE()' : 'NOW()';

        $db->prepare("UPDATE house_bls SET status='READY_FOR_DO', updated_at={$nowExpr} WHERE id=?")
           ->execute([$houseId]);
      }

      $db->commit();

      echo json_encode([
        'success'     => 1,
        'payment_id'  => $paymentId,
        'receipt_no'  => $receiptNo,
        'house_bl_id' => $houseId,
        'invoice_id'  => $invoiceId
      ]);

    } catch (Throwable $e) {
      if ($db->inTransaction()) {
        $db->rollBack();
      }
      throw $e;
    }

  } catch (Throwable $e) {
    http_response_code(400);
    echo json_encode([
      'success' => 0,
      'message' => $e->getMessage()
    ]);
  }
})(), 
  

    '/api/do/issue' => fn() => (function() {
      global $db, $auth;
      $auth->requireLogin();
      require_post();

      $houseId = (int)($_POST['house_bl_id'] ?? 0);
      if ($houseId<=0) json_out(['ok'=>false,'msg'=>'Invalid HBL'], 400);

      // One DO per HBL
      $chk = $db->prepare("SELECT id FROM delivery_orders WHERE house_bl_id=? LIMIT 1");
      $chk->execute([$houseId]);
      if ($chk->fetch()) json_out(['ok'=>false,'msg'=>'DO already issued for this HBL'], 400);

      // Generate DO number
      $seq = next_number($db, 'do');
      $doNo = 'DO-' . date('Y') . '-' . $seq;

      $db->beginTransaction();
      $db->prepare("INSERT INTO delivery_orders(house_bl_id, do_no, status, issued_at, created_by)
                    VALUES(?, ?, 'ISSUED', NOW(), ?)")
        ->execute([$houseId, $doNo, (int)($_SESSION['uid'] ?? 1)]);

      // Move HBL forward
      $db->prepare("UPDATE house_bls SET status='DO_ISSUED', updated_at=NOW() WHERE id=?")->execute([$houseId]);

      $db->commit();

      json_out(['ok'=>true,'message'=>'Delivery Order issued: '.$doNo]);
    })(),

    '/api/invoice/mark-paid' => fn() => (function() {
      global $db, $auth;
      $auth->requireLogin();
      require_post();

      $invoiceId = (int)($_POST['invoice_id'] ?? 0);
      if ($invoiceId<=0) json_out(['ok'=>false,'msg'=>'Invalid invoice'], 400);

      $inv = $db->prepare("SELECT id, house_bl_id FROM invoices WHERE id=? LIMIT 1");
      $inv->execute([$invoiceId]);
      $row = $inv->fetch();
      if (!$row) json_out(['ok'=>false,'msg'=>'Invoice not found'], 404);

      $db->beginTransaction();
      $db->prepare("UPDATE invoices SET status='PAID' WHERE id=?")->execute([$invoiceId]);

      // Move HBL to READY_FOR_DO
      $db->prepare("UPDATE house_bls SET status='READY_FOR_DO', updated_at=NOW() WHERE id=?")
        ->execute([(int)$row['house_bl_id']]);

      $db->commit();

      json_out(['ok'=>true,'message'=>'Invoice marked PAID. HBL is READY_FOR_DO.']);
    })(),

    '/api/users/create' => fn() => (function() {
  global $db, $auth;
  $auth->requireLogin();
  require_post();

  $fullName = trim((string)($_POST['full_name'] ?? ''));
  $email = trim((string)($_POST['email'] ?? ''));
  $phone = trim((string)($_POST['phone'] ?? ''));
  $password = (string)($_POST['password'] ?? '');
  $passwordConfirm = (string)($_POST['password_confirm'] ?? '');
  $isActive = (int)($_POST['is_active'] ?? 1);

  if ($fullName === '') json_out(['ok'=>false,'msg'=>'Full name is required'], 400);
  if ($email === '') json_out(['ok'=>false,'msg'=>'Email is required'], 400);
  if (!filter_var($email, FILTER_VALIDATE_EMAIL)) json_out(['ok'=>false,'msg'=>'Invalid email address'], 400);
  if ($password === '') json_out(['ok'=>false,'msg'=>'Password is required'], 400);
  if (strlen($password) < 8) json_out(['ok'=>false,'msg'=>'Password must be at least 8 characters'], 400);
  if ($password !== $passwordConfirm) json_out(['ok'=>false,'msg'=>'Password confirmation does not match'], 400);

  $chk = $db->prepare("SELECT id FROM users WHERE email=? LIMIT 1");
  $chk->execute([$email]);
  if ($chk->fetch()) json_out(['ok'=>false,'msg'=>'A user with that email already exists'], 409);

  $passwordHash = password_hash($password, PASSWORD_DEFAULT);

  $stmt = $db->prepare("
    INSERT INTO users (full_name, email, phone, password_hash, is_active)
    VALUES (?, ?, ?, ?, ?)
  ");
  $stmt->execute([
    $fullName,
    $email,
    ($phone !== '' ? $phone : null),
    $passwordHash,
    $isActive ? 1 : 0
  ]);

  json_out([
    'ok'=>true,
    'redirect'=>to('/users')
  ]);
})(),

'/api/users/update' => fn() => (function() {
  global $db, $auth;
  $auth->requireLogin();
  require_post();

  $id = (int)($_POST['id'] ?? 0);
  $fullName = trim((string)($_POST['full_name'] ?? ''));
  $email = trim((string)($_POST['email'] ?? ''));
  $phone = trim((string)($_POST['phone'] ?? ''));
  $isActive = (int)($_POST['is_active'] ?? 1);

  if ($id <= 0) json_out(['ok'=>false,'msg'=>'Invalid user'], 400);
  if ($fullName === '') json_out(['ok'=>false,'msg'=>'Full name is required'], 400);
  if ($email === '') json_out(['ok'=>false,'msg'=>'Email is required'], 400);
  if (!filter_var($email, FILTER_VALIDATE_EMAIL)) json_out(['ok'=>false,'msg'=>'Invalid email address'], 400);

  $chk = $db->prepare("SELECT id FROM users WHERE email=? AND id<>? LIMIT 1");
  $chk->execute([$email, $id]);
  if ($chk->fetch()) json_out(['ok'=>false,'msg'=>'Another user already uses that email'], 409);

  $stmt = $db->prepare("
    UPDATE users
    SET full_name=?, email=?, phone=?, is_active=?
    WHERE id=?
  ");
  $stmt->execute([
    $fullName,
    $email,
    ($phone !== '' ? $phone : null),
    $isActive ? 1 : 0,
    $id
  ]);

  json_out([
    'ok'=>true,
    'redirect'=>to('/users')
  ]);
})(),

'/api/users/toggle-active' => fn() => (function() {
  global $db, $auth;
  $auth->requireLogin();
  require_post();

  $id = (int)($_POST['id'] ?? 0);
  $isActive = (int)($_POST['is_active'] ?? 0);

  if ($id <= 0) json_out(['ok'=>false,'msg'=>'Invalid user'], 400);

  $stmt = $db->prepare("UPDATE users SET is_active=? WHERE id=?");
  $stmt->execute([$isActive ? 1 : 0, $id]);

  json_out(['ok'=>true]);
})(),

'/api/users/reset-password' => fn() => (function() {
  global $db, $auth;
  $auth->requireLogin();
  require_post();

  $id = (int)($_POST['id'] ?? 0);
  $newPassword = (string)($_POST['new_password'] ?? '');
  $newPasswordConfirm = (string)($_POST['new_password_confirm'] ?? '');

  if ($id <= 0) json_out(['ok'=>false,'msg'=>'Invalid user'], 400);
  if ($newPassword === '') json_out(['ok'=>false,'msg'=>'New password is required'], 400);
  if (strlen($newPassword) < 8) json_out(['ok'=>false,'msg'=>'Password must be at least 8 characters'], 400);
  if ($newPassword !== $newPasswordConfirm) json_out(['ok'=>false,'msg'=>'Password confirmation does not match'], 400);

  $hash = password_hash($newPassword, PASSWORD_DEFAULT);

  $stmt = $db->prepare("UPDATE users SET password_hash=? WHERE id=?");
  $stmt->execute([$hash, $id]);

  json_out(['ok'=>true]);
})(),

'/api/users/change-password' => fn() => (function() {
  global $db, $auth;
  $auth->requireLogin();
  require_post();

  $uid = (int)($_SESSION['uid'] ?? $_SESSION['user_id'] ?? 0);
  if ($uid <= 0) json_out(['ok'=>false,'msg'=>'Session user not found'], 401);

  $currentPassword = (string)($_POST['current_password'] ?? '');
  $newPassword = (string)($_POST['new_password'] ?? '');
  $newPasswordConfirm = (string)($_POST['new_password_confirm'] ?? '');

  if ($currentPassword === '') json_out(['ok'=>false,'msg'=>'Current password is required'], 400);
  if ($newPassword === '') json_out(['ok'=>false,'msg'=>'New password is required'], 400);
  if (strlen($newPassword) < 8) json_out(['ok'=>false,'msg'=>'New password must be at least 8 characters'], 400);
  if ($newPassword !== $newPasswordConfirm) json_out(['ok'=>false,'msg'=>'New password confirmation does not match'], 400);

  $st = $db->prepare("SELECT id, password_hash FROM users WHERE id=? LIMIT 1");
  $st->execute([$uid]);
  $u = $st->fetch();

  if (!$u) json_out(['ok'=>false,'msg'=>'User account not found'], 404);

  if (!password_verify($currentPassword, (string)$u['password_hash'])) {
    json_out(['ok'=>false,'msg'=>'Current password is incorrect'], 400);
  }

  $newHash = password_hash($newPassword, PASSWORD_DEFAULT);

  $up = $db->prepare("UPDATE users SET password_hash=? WHERE id=?");
  $up->execute([$newHash, $uid]);

  json_out([
    'ok'=>true,
    'redirect'=>to('/change-password')
  ]);
})(),

    '/api/customers/create' => fn() => (function() {
      global $db;
      require_post();

      $name = trim((string)($_POST['name'] ?? ''));
      if ($name === '') json_out(['ok'=>false,'msg'=>'Customer name is required'], 400);

      $stmt = $db->prepare("INSERT INTO customers(name, contact_person, email, phone, address, kra_pin)
                            VALUES(?,?,?,?,?,?)");
      $stmt->execute([
        $name,
        ($_POST['contact_person'] ?? '') ?: null,
        ($_POST['email'] ?? '') ?: null,
        ($_POST['phone'] ?? '') ?: null,
        ($_POST['address'] ?? '') ?: null,
        ($_POST['kra_pin'] ?? '') ?: null,
      ]);

      json_out(['ok'=>true,'redirect'=>to('/customers')]);
    })(),

    '/api/customers/update' => fn() => (function() {
      global $db;
      require_post();

      $id = (int)($_POST['id'] ?? 0);
      if ($id <= 0) json_out(['ok'=>false,'msg'=>'Invalid customer'], 400);

      $name = trim((string)($_POST['name'] ?? ''));
      if ($name === '') json_out(['ok'=>false,'msg'=>'Customer name is required'], 400);

      $stmt = $db->prepare("UPDATE customers
                            SET name=?, contact_person=?, email=?, phone=?, address=?, kra_pin=?
                            WHERE id=?");
      $stmt->execute([
        $name,
        ($_POST['contact_person'] ?? '') ?: null,
        ($_POST['email'] ?? '') ?: null,
        ($_POST['phone'] ?? '') ?: null,
        ($_POST['address'] ?? '') ?: null,
        ($_POST['kra_pin'] ?? '') ?: null,
        $id
      ]);

      json_out(['ok'=>true,'redirect'=>to('/customers')]);
    })(),

    '/api/customers/delete' => fn() => (function() {
      global $db;
      require_post();

      $id = (int)($_POST['id'] ?? 0);
      if ($id <= 0) json_out(['ok'=>false,'msg'=>'Invalid customer'], 400);

      // Guard: prevent delete if referenced by HBLs
      $chk = $db->prepare("SELECT COUNT(*) c FROM house_bls
                           WHERE consignee_customer_id=? OR billing_party_customer_id=?");
      $chk->execute([$id, $id]);
      $used = (int)$chk->fetch()['c'];

      if ($used > 0) {
        json_out(['ok'=>false,'msg'=>'Cannot delete: customer is used in HBL records. Edit the HBL(s) first.'], 409);
      }

      $stmt = $db->prepare("DELETE FROM customers WHERE id=?");
      $stmt->execute([$id]);

      json_out(['ok'=>true,'redirect'=>to('/customers')]);
    })(),

    '/api/invoice/generate' => fn() => (function() {
      global $db;
      require_post();

      $houseId = (int)($_POST['house_bl_id'] ?? 0);
      if ($houseId <= 0) json_out(['ok'=>false,'msg'=>'Missing house_bl_id'], 400);

      // If an invoice already exists for this HBL, reuse it
      $existing = $db->prepare("SELECT id FROM invoices WHERE house_bl_id=? ORDER BY id DESC LIMIT 1");
      $existing->execute([$houseId]);
      $ex = $existing->fetch();

      if ($ex) {
          $invoiceId = (int)$ex['id'];
          json_out([
            'ok' => true,
            // keep SPA compatible
            'redirect' => to('/invoice/view?id=' . $invoiceId),
            // also provide standalone doc URL
            'url' => to('/invoice.php?id=' . $invoiceId),
            'invoice_id' => $invoiceId,
            'existing' => true,
          ]);
        }

        // No invoice exists: go to charge selection grid (SPA partial)
        json_out([
          'ok' => true,
          'redirect' => to('/invoice/select-charges?house_bl_id=' . $houseId),
          'house_bl_id' => $houseId,
          'existing' => false,
        ]);
      })(),

    '/api/invoice/create-selected' => fn() => (function() {
        global $db;
        require_post();
    
        $houseId = (int)($_POST['house_bl_id'] ?? 0);
        $lines   = $_POST['lines'] ?? [];
    
        if ($houseId <= 0) {
          json_out(['ok' => false, 'msg' => 'Missing house_bl_id'], 400);
        }
    
        if (!is_array($lines) || !$lines) {
          json_out(['ok' => false, 'msg' => 'No charge lines submitted.'], 422);
        }
    
        // Prevent duplicates
        $existing = $db->prepare("SELECT id FROM invoices WHERE house_bl_id=? ORDER BY id DESC LIMIT 1");
        $existing->execute([$houseId]);
        $ex = $existing->fetch();
    
        if ($ex) {
          $invoiceId = (int)$ex['id'];
          json_out([
            'ok'         => true,
            'invoice_id' => $invoiceId,
            'url'        => to('/invoice.php?id=' . $invoiceId),
            'redirect'   => to('/invoice/view?id=' . $invoiceId),
            'existing'   => true
          ]);
        }
    
        // Load HBL
        $h = $db->prepare("
          SELECT h.*
          FROM house_bls h
          WHERE h.id=?
        ");
        $h->execute([$houseId]);
        $hbl = $h->fetch();
    
        if (!$hbl) {
          json_out(['ok' => false, 'msg' => 'HBL not found'], 404);
        }
    
        // Whitelist valid charge type ids from master table
        $validChargeIds = $db->query("SELECT id FROM charge_types")->fetchAll(PDO::FETCH_COLUMN);
        $validChargeIds = array_map('intval', $validChargeIds);
        $validChargeMap = array_fill_keys($validChargeIds, true);
    
        // Normalize submitted lines
        $selectedLines = [];
    
        foreach ($lines as $row) {
          $use = (int)($row['use'] ?? 0);
          if ($use !== 1) {
            continue;
          }
    
          $chargeTypeId = (int)($row['charge_type_id'] ?? 0);
          $basis        = strtoupper(trim((string)($row['basis'] ?? 'FLAT')));
          $name         = trim((string)($row['name'] ?? ''));
          $rate         = (float)($row['rate'] ?? 0);
          $qty          = (float)($row['qty'] ?? 0);
    
          if ($chargeTypeId <= 0 || !isset($validChargeMap[$chargeTypeId])) {
            continue;
          }
    
          if ($name === '') {
            json_out(['ok' => false, 'msg' => 'Every selected charge must have a name.'], 422);
          }
    
          if ($rate < 0) {
            json_out(['ok' => false, 'msg' => 'Charge rate cannot be negative.'], 422);
          }
    
          if ($qty <= 0) {
            json_out(['ok' => false, 'msg' => 'Charge quantity must be greater than zero.'], 422);
          }
    
          if (!in_array($basis, ['FLAT', 'PER_UNIT_WM'], true)) {
            $basis = 'FLAT';
          }
    
          $amount = round($qty * $rate, 2);
    
          $selectedLines[] = [
            'charge_type_id' => $chargeTypeId,
            'description'    => $name,
            'basis'          => $basis,
            'qty'            => $qty,
            'unit_price'     => $rate,
            'amount'         => $amount,
          ];
        }
    
        if (!$selectedLines) {
          json_out(['ok' => false, 'msg' => 'Select at least one valid charge item to generate invoice.'], 422);
        }
    
            if (!function_exists('wsc_vessel_initials')) {
      function wsc_vessel_initials(string $name): string {
        $name = strtoupper(trim($name));
        if ($name === '') return 'XX';

        $name = preg_replace('/[^A-Z0-9 ]+/', ' ', $name);
        $parts = preg_split('/\s+/', $name, -1, PREG_SPLIT_NO_EMPTY);

        if (!$parts) return 'XX';

        $initials = '';
        foreach ($parts as $p) {
          $initials .= substr($p, 0, 1);
        }

        return $initials ?: 'XX';
      }
    }

    if (!function_exists('wsc_generate_invoice_no')) {
      function wsc_generate_invoice_no(PDO $db, array $hbl): string {
        $m = $db->prepare("
          SELECT vessel_name, voyage_no
          FROM master_bls
          WHERE id = ?
          LIMIT 1
        ");
        $m->execute([(int)($hbl['master_bl_id'] ?? 0)]);
        $mbl = $m->fetch();

        $vesselName = (string)($mbl['vessel_name'] ?? '');
        $voyageNo   = trim((string)($mbl['voyage_no'] ?? ''));

        $initials = wsc_vessel_initials($vesselName);
        $middle   = $initials . $voyageNo;

        if ($middle === '') {
          $middle = 'XX000';
        }

        $dt = new DateTime();
        return 'WSC-' . $middle . '/' . $dt->format('Hi') . '/' . $dt->format('m') . '/' . $dt->format('y');
      }
    }

    $invNo = wsc_generate_invoice_no($db, $hbl);
    
        $createdBy = (int)($_SESSION['uid'] ?? 0);
        if ($createdBy <= 0) {
          $createdBy = (int)($_SESSION['user_id'] ?? 1);
        }
    
        $driver = $db->getAttribute(PDO::ATTR_DRIVER_NAME);
        $todayExpr = ($driver === 'sqlsrv') ? 'CAST(GETDATE() AS DATE)' : 'CURDATE()';
        $nowExpr   = ($driver === 'sqlsrv') ? 'GETDATE()' : 'NOW()';
    
        $db->beginTransaction();
    
        try {
          // Create invoice
          $stmt = $db->prepare("
            INSERT INTO invoices(
              invoice_no,
              house_bl_id,
              consignee_customer_id,
              billing_party_customer_id,
              currency,
              status,
              issue_date,
              created_by
            )
            VALUES(
              ?, ?, ?, ?, 'USD', 'ISSUED', {$todayExpr}, ?
            )
          ");
          $stmt->execute([
            $invNo,
            $houseId,
            (int)$hbl['consignee_customer_id'],
            (int)$hbl['billing_party_customer_id'],
            $createdBy
          ]);
    
          $invoiceId = (int)$db->lastInsertId();
    
          $subtotal = 0.0;
    
          $ins = $db->prepare("
            INSERT INTO invoice_items(
              invoice_id,
              charge_type_id,
              description,
              basis,
              qty,
              unit_price,
              amount
            )
            VALUES(?,?,?,?,?,?,?)
          ");
    
          foreach ($selectedLines as $line) {
            $subtotal += (float)$line['amount'];
    
            $ins->execute([
              $invoiceId,
              $line['charge_type_id'],
              $line['description'],
              $line['basis'],
              $line['qty'],
              $line['unit_price'],
              $line['amount']
            ]);
          }
    
          $subtotal = round($subtotal, 2);
          $total    = round($subtotal, 2);
    
          $db->prepare("UPDATE invoices SET subtotal=?, total=? WHERE id=?")
             ->execute([$subtotal, $total, $invoiceId]);
    
          // Advance HBL lifecycle
          $db->prepare("
            UPDATE house_bls
            SET status='AWAITING_PAYMENT', updated_at={$nowExpr}
            WHERE id=?
          ")->execute([$houseId]);
    
          // Optional: audit (if invoice_events exists)
          $eventMeta = [
            'mode'  => 'selected-editable-lines',
            'lines' => array_map(function ($line) {
              return [
                'charge_type_id' => $line['charge_type_id'],
                'description'    => $line['description'],
                'basis'          => $line['basis'],
                'qty'            => $line['qty'],
                'unit_price'     => $line['unit_price'],
                'amount'         => $line['amount'],
              ];
            }, $selectedLines)
          ];
    
          $db->prepare("
            INSERT INTO invoice_events (invoice_id, event_type, user_id, ip_address, user_agent, meta)
            VALUES (?, 'CREATED_FROM_SELECTED_LINES', ?, ?, ?, ?)
          ")->execute([
            $invoiceId,
            ($_SESSION['uid'] ?? $_SESSION['user_id'] ?? null),
            ($_SERVER['REMOTE_ADDR'] ?? null),
            ($_SERVER['HTTP_USER_AGENT'] ?? null),
            json_encode($eventMeta, JSON_UNESCAPED_UNICODE)
          ]);
    
          $db->commit();
    
          json_out([
            'ok'         => true,
            'invoice_id' => $invoiceId,
            'url'        => to('/invoice.php?id=' . $invoiceId),
            'redirect'   => to('/invoice/view?id=' . $invoiceId),
          ]);
    
        } catch (Throwable $e) {
          if ($db->inTransaction()) {
            $db->rollBack();
          }
    
          json_out(['ok' => false, 'msg' => 'Invoice creation failed'], 500);
        }
      })(),

    ],
  ];

if (!isset($routes[$method][$path])) {
  http_response_code(404);
  echo "404 Not Found";
  exit;
}

$routes[$method][$path]();