<?php
require_once __DIR__ . '/../config.php';
require_once __DIR__ . '/../includes/auth.php';
require_once __DIR__ . '/../includes/db.php';
require_login();
header('Content-Type: application/json');
$mysqli = db();
$action = $_GET['action'] ?? $_POST['action'] ?? 'list';

function ensure_admin_invoice(){ /* role enforcement disabled: all logged-in users allowed */ return; }

function sess_branch_id(){ return $_SESSION['branch_id'] ?? null; }
function is_admin_user(){ return has_role(['admin','super_admin']); }

// Helper: Convert amount to words (simplified) - needed for cash book updates
function amount_in_words($amount, $currency = 'LKR') {
    $amount = floatval($amount);
    $whole = floor($amount);
    $decimal = round(($amount - $whole) * 100);
    
    $ones = ['', 'One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine'];
    $teens = ['Ten', 'Eleven', 'Twelve', 'Thirteen', 'Fourteen', 'Fifteen', 'Sixteen', 'Seventeen', 'Eighteen', 'Nineteen'];
    $tens = ['', '', 'Twenty', 'Thirty', 'Forty', 'Fifty', 'Sixty', 'Seventy', 'Eighty', 'Ninety'];
    $places = ['', 'Thousand', 'Million', 'Billion'];
    
    $convert_number = function($num) use (&$convert_number, $ones, $teens, $tens) {
        if ($num < 10) return $ones[$num];
        if ($num < 20) return $teens[$num - 10];
        if ($num < 100) return $tens[floor($num / 10)] . ($num % 10 ? ' ' . $ones[$num % 10] : '');
        if ($num < 1000) return $ones[floor($num / 100)] . ' Hundred' . ($num % 100 ? ' and ' . $convert_number($num % 100) : '');
        return '';
    };
    
    $convert_whole = function($num) use ($places, $convert_number) {
        if ($num == 0) return 'Zero';
        $parts = [];
        $place = 0;
        while ($num > 0) {
            $chunk = $num % 1000;
            if ($chunk > 0) {
                $part = $convert_number($chunk);
                if (isset($places[$place]) && $places[$place]) {
                    $part .= ' ' . $places[$place];
                }
                array_unshift($parts, $part);
            }
            $num = floor($num / 1000);
            $place++;
        }
        return implode(', ', $parts);
    };
    
    $words = $convert_whole($whole);
    if ($decimal > 0) {
        $words .= ' and ' . $convert_number($decimal) . ' Cents';
    }
    
    $currencyNames = ['LKR' => 'Sri Lankan Rupees', 'USD' => 'US Dollars'];
    return $words . ' ' . ($currencyNames[$currency] ?? $currency);
}

// Ensure invoice_items has cut_piece_id column for slab cutting integration
function ensure_invoice_schema(mysqli $db){
  // Add cut_piece_id to invoice_items (for slab cutting integration)
  $res = $db->query("SHOW COLUMNS FROM invoice_items LIKE 'cut_piece_id'");
  if (!$res || !$res->fetch_assoc()) {
    @$db->query("ALTER TABLE invoice_items ADD COLUMN cut_piece_id INT NULL AFTER final_product_id");
  }
  // Add payment_term to invoices
  $res2 = $db->query("SHOW COLUMNS FROM invoices LIKE 'payment_term'");
  if (!$res2 || !$res2->fetch_assoc()) {
    @$db->query("ALTER TABLE invoices ADD COLUMN payment_term VARCHAR(20) NULL AFTER status");
  }
  // Add discount_percent to invoices
  $res3 = $db->query("SHOW COLUMNS FROM invoices LIKE 'discount_percent'");
  if (!$res3 || !$res3->fetch_assoc()) {
    @$db->query("ALTER TABLE invoices ADD COLUMN discount_percent DECIMAL(6,3) NULL AFTER tax_amount");
  }
  // Add transport_charges to invoices
  $res4 = $db->query("SHOW COLUMNS FROM invoices LIKE 'transport_charges'");
  if (!$res4 || !$res4->fetch_assoc()) {
    @$db->query("ALTER TABLE invoices ADD COLUMN transport_charges DECIMAL(12,2) NOT NULL DEFAULT 0 AFTER tax_amount");
  }
  // Add production_item_id to invoice_items
  $res5 = $db->query("SHOW COLUMNS FROM invoice_items LIKE 'production_item_id'");
  if (!$res5 || !$res5->fetch_assoc()) {
    @$db->query("ALTER TABLE invoice_items ADD COLUMN production_item_id INT NULL AFTER cut_piece_id");
  }
  // Add returned_qty to invoice_items
  $res6 = $db->query("SHOW COLUMNS FROM invoice_items LIKE 'returned_qty'");
  if (!$res6 || !$res6->fetch_assoc()) {
    @$db->query("ALTER TABLE invoice_items ADD COLUMN returned_qty DECIMAL(12,3) NOT NULL DEFAULT 0 AFTER qty");
  }
  // Add service_id to invoice_items
  $res8 = $db->query("SHOW COLUMNS FROM invoice_items LIKE 'service_id'");
  if (!$res8 || !$res8->fetch_assoc()) {
    @$db->query("ALTER TABLE invoice_items ADD COLUMN service_id INT NULL AFTER production_item_id");
  }
  // Add length_ft to invoice_items
  $res9 = $db->query("SHOW COLUMNS FROM invoice_items LIKE 'length_ft'");
  if (!$res9 || !$res9->fetch_assoc()) {
    @$db->query("ALTER TABLE invoice_items ADD COLUMN length_ft DECIMAL(12,3) NULL AFTER qty");
  }
  // Add payment reversal columns to payments
  $res10 = $db->query("SHOW COLUMNS FROM payments LIKE 'payment_type'");
  if (!$res10 || !$res10->fetch_assoc()) {
    @$db->query("ALTER TABLE payments ADD COLUMN payment_type ENUM('normal','reversal') NOT NULL DEFAULT 'normal' AFTER notes");
  }
  $res11 = $db->query("SHOW COLUMNS FROM payments LIKE 'reversed_payment_id'");
  if (!$res11 || !$res11->fetch_assoc()) {
    @$db->query("ALTER TABLE payments ADD COLUMN reversed_payment_id INT NULL AFTER payment_type");
  }
  $res12 = $db->query("SHOW COLUMNS FROM payments LIKE 'is_reversed'");
  if (!$res12 || !$res12->fetch_assoc()) {
    @$db->query("ALTER TABLE payments ADD COLUMN is_reversed TINYINT(1) NOT NULL DEFAULT 0 AFTER reversed_payment_id");
  }
  $res7 = $db->query("SHOW COLUMNS FROM invoices LIKE 'branch_id'");
  if (!$res7 || !$res7->fetch_assoc()) {
    @$db->query("ALTER TABLE invoices ADD COLUMN branch_id INT NULL AFTER notes");
  }
  // Approval workflow table
  $db->query("CREATE TABLE IF NOT EXISTS invoice_approvals (
    id INT AUTO_INCREMENT PRIMARY KEY,
    invoice_id INT NOT NULL,
    action ENUM('cancel','amend') NOT NULL,
    reason VARCHAR(255) NULL,
    status ENUM('pending','approved','rejected') NOT NULL DEFAULT 'pending',
    requested_by INT NULL,
    approved_by INT NULL,
    used TINYINT(1) NOT NULL DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    decided_at TIMESTAMP NULL,
    CONSTRAINT fk_inv_appr_inv FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE CASCADE
  ) ENGINE=InnoDB");
  // Item returns log
  $db->query("CREATE TABLE IF NOT EXISTS invoice_return_items (
    id INT AUTO_INCREMENT PRIMARY KEY,
    invoice_item_id INT NOT NULL,
    qty_returned DECIMAL(12,3) NOT NULL,
    reason VARCHAR(255) NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    CONSTRAINT fk_inv_ret_item FOREIGN KEY (invoice_item_id) REFERENCES invoice_items(id) ON DELETE CASCADE
  ) ENGINE=InnoDB");
}

function inv_branch_code($mysqli){
  $bid = (int)(sess_branch_id() ?? 0);
  $name = $_SESSION['branch_name'] ?? '';
  $code = '';
  if ($name !== ''){
    $parts = preg_split('/\s+/', trim($name));
    foreach($parts as $p){
      $p = preg_replace('/[^A-Za-z0-9]/','',$p);
      if($p!=='') $code .= strtoupper(substr($p,0,1));
    }
  }
  // If code is empty or just generic, use branch-based default
  if ($code === ''){
    $code = $bid>0 ? ('B'.$bid) : 'MAIN';
  }
  return $code;
}

function inv_generate_no($mysqli){
  $bid = (int)(sess_branch_id() ?? 0);
  $code = inv_branch_code($mysqli);
  $prefix = 'INV-' . $code . '-' . date('Ym') . '-';
  $like = $prefix . '%';
  if ($bid > 0){
    $stmt = $mysqli->prepare('SELECT invoice_no FROM invoices WHERE branch_id=? AND invoice_no LIKE ? ORDER BY invoice_no DESC LIMIT 1');
    $stmt->bind_param('is',$bid,$like);
  } else {
    $stmt = $mysqli->prepare('SELECT invoice_no FROM invoices WHERE invoice_no LIKE ? ORDER BY invoice_no DESC LIMIT 1');
    $stmt->bind_param('s',$like);
  }
  $stmt->execute(); $row=$stmt->get_result()->fetch_assoc();
  $n=1; if ($row && preg_match('/^INV\-[A-Z0-9]+\-\d{6}\-(\d{4})$/',$row['invoice_no'],$m)) $n=((int)$m[1])+1;
  return $prefix . str_pad((string)$n,4,'0',STR_PAD_LEFT);
}

ensure_invoice_schema($mysqli);
switch ($action) {
  case 'generate_no':
    ensure_admin_invoice();
    json_response(['invoice_no'=>inv_generate_no($mysqli)]);
    break;

  case 'add_item':
    if (!validate_csrf($_POST['csrf'] ?? '')) json_response(['message'=>'Invalid CSRF'], 400);
    $invoice_id=(int)($_POST['invoice_id'] ?? 0); if(!$invoice_id) json_response(['message'=>'Missing invoice_id'],400);
    $inv=$mysqli->query('SELECT status, branch_id FROM invoices WHERE id='.(int)$invoice_id)->fetch_assoc(); if(!$inv) json_response(['message'=>'Invoice not found'],404);
    if (!is_admin_user()){
      $bid=(int)(sess_branch_id() ?? 0); if ((int)($inv['branch_id'] ?? 0) !== $bid) json_response(['message'=>'Forbidden'],403);
    }
    $fp_id = ($_POST['final_product_id'] ?? '')!=='' ? (int)$_POST['final_product_id'] : null;
    $cp_id = ($_POST['cut_piece_id'] ?? '')!=='' ? (int)$_POST['cut_piece_id'] : null;
    $pi_id = ($_POST['production_item_id'] ?? '')!=='' ? (int)$_POST['production_item_id'] : null;
    $sv_id = ($_POST['service_id'] ?? '')!=='' ? (int)$_POST['service_id'] : null;
    $desc = trim($_POST['description'] ?? 'Item');
    $qty = (float)($_POST['qty'] ?? 1);
    $length_ft = isset($_POST['length_ft']) && $_POST['length_ft'] !== '' ? (float)$_POST['length_ft'] : null;
    if ($length_ft !== null) { $qty = $length_ft; }
    $unit_price = (float)($_POST['unit_price'] ?? 0);
    $returned_qty = 0.0;
    $line_total = round(max(0, $qty - $returned_qty) * $unit_price, 2);
    $stmt=$mysqli->prepare('INSERT INTO invoice_items (invoice_id, final_product_id, cut_piece_id, production_item_id, service_id, description, qty, length_ft, returned_qty, unit_price, line_total) VALUES (?,?,?,?,?,?,?,?,?,?,?)');
    $stmt->bind_param('iiiiisdddds', $invoice_id, $fp_id, $cp_id, $pi_id, $sv_id, $desc, $qty, $length_ft, $returned_qty, $unit_price, $line_total);
    if(!$stmt->execute()) json_response(['message'=>$stmt->error],500);
    $item_id = $mysqli->insert_id;
    if ($fp_id) { $mysqli->query('UPDATE final_products SET sale_status="reserved" WHERE id='.(int)$fp_id); }
    if ($cp_id) { $mysqli->query('UPDATE cut_pieces SET is_available=0, reserved_invoice_item_id='.(int)$item_id.' WHERE id='.(int)$cp_id); }
    if ($pi_id) { $mysqli->query('UPDATE production_items SET availability="reserved" WHERE id='.(int)$pi_id); }
    // recompute totals
    $sum=$mysqli->query('SELECT IFNULL(SUM(line_total),0) s FROM invoice_items WHERE invoice_id='.(int)$invoice_id)->fetch_assoc();
    $subtotal=(float)$sum['s'];
    $i=$mysqli->query('SELECT tax_percent, transport_charges, discount_percent, discount FROM invoices WHERE id='.(int)$invoice_id)->fetch_assoc();
    $tax_amount = round($subtotal * (float)$i['tax_percent'] / 100, 2);
    $disc = ($i['discount_percent']!==null) ? round($subtotal * (float)$i['discount_percent'] / 100, 2) : (float)$i['discount'];
    $grand_total = max(0, round($subtotal + $tax_amount + (float)$i['transport_charges'] - $disc, 2));
    $up=$mysqli->prepare('UPDATE invoices SET subtotal=?, tax_amount=?, grand_total=? WHERE id=?');
    $up->bind_param('dddi',$subtotal,$tax_amount,$grand_total,$invoice_id); @$up->execute();
    json_response(['message'=>'Item added','item_id'=>$item_id]);
    break;

  case 'update_item':
    if (!validate_csrf($_POST['csrf'] ?? '')) json_response(['message'=>'Invalid CSRF'], 400);
    $item_id=(int)($_POST['id'] ?? 0); if(!$item_id) json_response(['message'=>'Missing item id'],400);
    $row=$mysqli->query('SELECT * FROM invoice_items WHERE id='.(int)$item_id)->fetch_assoc(); if(!$row) json_response(['message'=>'Item not found'],404);
    $invoice_id=(int)$row['invoice_id'];
    $inv=$mysqli->query('SELECT status, branch_id FROM invoices WHERE id='.(int)$invoice_id)->fetch_assoc(); if(!$inv) json_response(['message'=>'Invoice not found'],404);
    if (!is_admin_user()){
      $bid=(int)(sess_branch_id() ?? 0); if ((int)($inv['branch_id'] ?? 0) !== $bid) json_response(['message'=>'Forbidden'],403);
    }
    $new_fp = ($_POST['final_product_id'] ?? '')!=='' ? (int)$_POST['final_product_id'] : null;
    $new_cp = ($_POST['cut_piece_id'] ?? '')!=='' ? (int)$_POST['cut_piece_id'] : null;
    $new_pi = ($_POST['production_item_id'] ?? '')!=='' ? (int)$_POST['production_item_id'] : null;
    $new_sv = ($_POST['service_id'] ?? '')!=='' ? (int)$_POST['service_id'] : null;
    $desc = trim($_POST['description'] ?? $row['description']);
    $qty = isset($_POST['qty']) ? (float)$_POST['qty'] : (float)$row['qty'];
    $length_ft = isset($_POST['length_ft']) && $_POST['length_ft'] !== '' ? (float)$_POST['length_ft'] : ($row['length_ft'] ?? null);
    if ($length_ft !== null) { $qty = $length_ft; }
    $unit_price = isset($_POST['unit_price']) ? (float)$_POST['unit_price'] : (float)$row['unit_price'];
    $returned_qty = (float)$row['returned_qty'];
    // release previous reservations if changing linkage
    if ($row['final_product_id'] && $row['final_product_id'] != $new_fp) $mysqli->query('UPDATE final_products SET sale_status="available" WHERE id='.(int)$row['final_product_id']);
    if ($row['cut_piece_id'] && $row['cut_piece_id'] != $new_cp) $mysqli->query('UPDATE cut_pieces SET is_available=1, reserved_invoice_item_id=NULL WHERE id='.(int)$row['cut_piece_id']);
    if ($row['production_item_id'] && $row['production_item_id'] != $new_pi) $mysqli->query('UPDATE production_items SET availability="available" WHERE id='.(int)$row['production_item_id']);
    // reserve new
    if ($new_fp && $new_fp != $row['final_product_id']) $mysqli->query('UPDATE final_products SET sale_status="reserved" WHERE id='.(int)$new_fp);
    if ($new_cp && $new_cp != $row['cut_piece_id']) $mysqli->query('UPDATE cut_pieces SET is_available=0, reserved_invoice_item_id='.(int)$item_id.' WHERE id='.(int)$new_cp);
    if ($new_pi && $new_pi != $row['production_item_id']) $mysqli->query('UPDATE production_items SET availability="reserved" WHERE id='.(int)$new_pi);
    $line_total = round(max(0, $qty - $returned_qty) * $unit_price, 2);
    $u=$mysqli->prepare('UPDATE invoice_items SET final_product_id=?, cut_piece_id=?, production_item_id=?, service_id=?, description=?, qty=?, length_ft=?, unit_price=?, line_total=? WHERE id=?');
    $u->bind_param('iiiisddddi', $new_fp, $new_cp, $new_pi, $new_sv, $desc, $qty, $length_ft, $unit_price, $line_total, $item_id);
    if(!$u->execute()) json_response(['message'=>$u->error],500);
    // recompute totals
    $sum=$mysqli->query('SELECT IFNULL(SUM(line_total),0) s FROM invoice_items WHERE invoice_id='.(int)$invoice_id)->fetch_assoc();
    $subtotal=(float)$sum['s'];
    $i=$mysqli->query('SELECT tax_percent, transport_charges, discount_percent, discount FROM invoices WHERE id='.(int)$invoice_id)->fetch_assoc();
    $tax_amount = round($subtotal * (float)$i['tax_percent'] / 100, 2);
    $disc = ($i['discount_percent']!==null) ? round($subtotal * (float)$i['discount_percent'] / 100, 2) : (float)$i['discount'];
    $grand_total = max(0, round($subtotal + $tax_amount + (float)$i['transport_charges'] - $disc, 2));
    $up=$mysqli->prepare('UPDATE invoices SET subtotal=?, tax_amount=?, grand_total=? WHERE id=?');
    $up->bind_param('dddi',$subtotal,$tax_amount,$grand_total,$invoice_id); @$up->execute();
    json_response(['message'=>'Item updated']);
    break;

  case 'delete_item':
    if (!validate_csrf($_POST['csrf'] ?? '')) json_response(['message'=>'Invalid CSRF'], 400);
    $item_id=(int)($_POST['id'] ?? 0); if(!$item_id) json_response(['message'=>'Missing item id'],400);
    $row=$mysqli->query('SELECT * FROM invoice_items WHERE id='.(int)$item_id)->fetch_assoc(); if(!$row) json_response(['message'=>'Item not found'],404);
    $invoice_id=(int)$row['invoice_id'];
    $inv=$mysqli->query('SELECT status, branch_id FROM invoices WHERE id='.(int)$invoice_id)->fetch_assoc(); if(!$inv) json_response(['message'=>'Invoice not found'],404);
    if (!is_admin_user()){
      $bid=(int)(sess_branch_id() ?? 0); if ((int)($inv['branch_id'] ?? 0) !== $bid) json_response(['message'=>'Forbidden'],403);
    }
    // release reservations
    if ($row['final_product_id']) $mysqli->query('UPDATE final_products SET sale_status="available" WHERE id='.(int)$row['final_product_id']);
    if ($row['cut_piece_id']) $mysqli->query('UPDATE cut_pieces SET is_available=1, reserved_invoice_item_id=NULL WHERE id='.(int)$row['cut_piece_id']);
    if ($row['production_item_id']) $mysqli->query('UPDATE production_items SET availability="available" WHERE id='.(int)$row['production_item_id']);
    $mysqli->query('DELETE FROM invoice_items WHERE id='.(int)$item_id);
    // recompute totals
    $sum=$mysqli->query('SELECT IFNULL(SUM(line_total),0) s FROM invoice_items WHERE invoice_id='.(int)$invoice_id)->fetch_assoc();
    $subtotal=(float)$sum['s'];
    $i=$mysqli->query('SELECT tax_percent, transport_charges, discount_percent, discount FROM invoices WHERE id='.(int)$invoice_id)->fetch_assoc();
    $tax_amount = round($subtotal * (float)$i['tax_percent'] / 100, 2);
    $disc = ($i['discount_percent']!==null) ? round($subtotal * (float)$i['discount_percent'] / 100, 2) : (float)$i['discount'];
    $grand_total = max(0, round($subtotal + $tax_amount + (float)$i['transport_charges'] - $disc, 2));
    $up=$mysqli->prepare('UPDATE invoices SET subtotal=?, tax_amount=?, grand_total=? WHERE id=?');
    $up->bind_param('dddi',$subtotal,$tax_amount,$grand_total,$invoice_id); @$up->execute();
    json_response(['message'=>'Item deleted']);
    break;

  case 'delete':
    if (!validate_csrf($_POST['csrf'] ?? '')) json_response(['message'=>'Invalid CSRF'], 400);
    $invoice_id=(int)($_POST['id'] ?? 0); if(!$invoice_id) json_response(['message'=>'Missing id'],400);
    // Only superadmin can delete invoices
    if (!has_role(['super_admin'])) json_response(['message'=>'Only superadmin can delete invoices'],403);
    $inv=$mysqli->query('SELECT status, branch_id FROM invoices WHERE id='.(int)$invoice_id)->fetch_assoc(); if(!$inv) json_response(['message'=>'Not found'],404);
    // Only cancelled invoices can be deleted
    if ($inv['status']!=='cancelled') json_response(['message'=>'Only cancelled invoices can be deleted'],400);
    // release all reservations (revert items to original status)
    $resOld = $mysqli->query('SELECT final_product_id, cut_piece_id, production_item_id FROM invoice_items WHERE invoice_id='.(int)$invoice_id);
    while($resOld && ($r=$resOld->fetch_assoc())){
      if ($r['final_product_id']) { $mysqli->query('UPDATE final_products SET sale_status="available" WHERE id='.(int)$r['final_product_id']); }
      if ($r['cut_piece_id']) { $mysqli->query('UPDATE cut_pieces SET is_available=1, reserved_invoice_item_id=NULL WHERE id='.(int)$r['cut_piece_id']); }
      if ($r['production_item_id']) { $mysqli->query('UPDATE production_items SET availability="available" WHERE id='.(int)$r['production_item_id']); }
    }
    $mysqli->query('DELETE FROM invoice_items WHERE invoice_id='.(int)$invoice_id);
    $mysqli->query('DELETE FROM payments WHERE invoice_id='.(int)$invoice_id);
    $mysqli->query('DELETE FROM invoices WHERE id='.(int)$invoice_id);
    json_response(['message'=>'Invoice deleted']);
    break;

  case 'create':
    ensure_admin_invoice();
    if (!validate_csrf($_POST['csrf'] ?? '')) json_response(['message'=>'Invalid CSRF'], 400);
    $customer_id = (int)($_POST['customer_id'] ?? 0);
    $invoice_date = $_POST['invoice_date'] ?? date('Y-m-d');
    $status = $_POST['status'] ?? 'draft';
    $payment_term = trim($_POST['payment_term'] ?? ''); // Cash/Credit/Cheque
    $tax_percent = (float)($_POST['tax_percent'] ?? 0);
    $discount_amount_input = $_POST['discount'] ?? '';
    $discount_percent_input = $_POST['discount_percent'] ?? '';
    $notes = trim($_POST['notes'] ?? '');
    $advance_amount = (float)($_POST['advance_amount'] ?? 0);
    $transport_charges = (float)($_POST['transport_charges'] ?? 0);
    $items_json = $_POST['items_json'] ?? '[]';
    $items = json_decode($items_json,true) ?: [];

    if (!$customer_id || !in_array($status,['draft','issued'],true)) json_response(['message'=>'Invalid input'],400);
    if (empty($items)) json_response(['message'=>'At least one item required'],400);

    $invoice_no = inv_generate_no($mysqli);
    $subtotal = 0.0;
    foreach ($items as $it){
      $qty = (float)($it['qty'] ?? 1);
      $price = (float)($it['unit_price'] ?? 0);
      $subtotal += $qty * $price;
    }
    $tax_amount = round($subtotal * $tax_percent / 100, 2);
    // compute discount by percent if provided, else use absolute
    $discount_percent = ($discount_percent_input !== '') ? (float)$discount_percent_input : null;
    if ($discount_percent !== null) {
      $discount = round($subtotal * $discount_percent / 100, 2);
    } else {
      $discount = ($discount_amount_input !== '') ? round((float)$discount_amount_input, 2) : 0.0;
    }
    $grand_total = max(0, round($subtotal + $tax_amount + $transport_charges - $discount, 2));

    // set branch_id for this invoice
    $branch_id = (int)(sess_branch_id() ?? 0);
    $stmt=$mysqli->prepare('INSERT INTO invoices (invoice_no, customer_id, invoice_date, status, payment_term, subtotal, tax_percent, tax_amount, transport_charges, discount_percent, discount, grand_total, currency, notes, branch_id) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,\'LKR\',?,?)');
   // Types: s i s s s d d d d d d d s i
    $stmt->bind_param('sisssdddddddsi', $invoice_no, $customer_id, $invoice_date, $status, $payment_term, $subtotal, $tax_percent, $tax_amount, $transport_charges, $discount_percent, $discount, $grand_total, $notes, $branch_id);
    if(!$stmt->execute()) json_response(['message'=>$stmt->error],500);
    $invoice_id = $mysqli->insert_id;

    // Insert items
    $ins = $mysqli->prepare('INSERT INTO invoice_items (invoice_id, final_product_id, cut_piece_id, production_item_id, service_id, description, qty, length_ft, unit_price, line_total) VALUES (?,?,?,?,?,?,?,?,?,?)');
    foreach ($items as $it){
      $fp_id = !empty($it['final_product_id']) ? (int)$it['final_product_id'] : null;
      $cp_id = !empty($it['cut_piece_id']) ? (int)$it['cut_piece_id'] : null;
      $pi_id = !empty($it['production_item_id']) ? (int)$it['production_item_id'] : null;
      $sv_id = !empty($it['service_id']) ? (int)$it['service_id'] : null;
      $desc = trim($it['description'] ?? 'Item');
      $length_ft = isset($it['length_ft']) ? (float)$it['length_ft'] : null;
      $qty = ($length_ft !== null) ? (float)$length_ft : (float)($it['qty'] ?? 1);
      $price = (float)($it['unit_price'] ?? 0);
      $total = round($qty * $price, 2);
      $ins->bind_param('iiiiisddds', $invoice_id, $fp_id, $cp_id, $pi_id, $sv_id, $desc, $qty, $length_ft, $price, $total);
      if(!$ins->execute()) json_response(['message'=>$ins->error],500);
      $item_id = $mysqli->insert_id;
      if ($fp_id) { $mysqli->query('UPDATE final_products SET sale_status="reserved" WHERE id='.(int)$fp_id); }
      if ($cp_id) { $mysqli->query('UPDATE cut_pieces SET is_available=0, reserved_invoice_item_id='.(int)$item_id.' WHERE id='.(int)$cp_id); }
      if ($pi_id) { $mysqli->query('UPDATE production_items SET availability="reserved" WHERE id='.(int)$pi_id); }
    }

    // Record advance payment if provided
    if ($advance_amount > 0) {
      $pay_date = $invoice_date;
      $method = ($payment_term !== '') ? $payment_term : 'Advance';
      $ref = 'Advance at create';
      $p = $mysqli->prepare('INSERT INTO payments (invoice_id, pay_date, method, amount, reference_no, notes) VALUES (?,?,?,?,?,?)');
      $nm = 'Advance';
      $p->bind_param('issdss', $invoice_id, $pay_date, $method, $advance_amount, $ref, $nm);
      @$p->execute();
      // Mark paid if fully covered by advance
      $sum=$mysqli->query('SELECT COALESCE(SUM(amount),0) s FROM payments WHERE invoice_id='.(int)$invoice_id)->fetch_assoc();
      $tot=(float)$sum['s'];
      $inv=$mysqli->query('SELECT grand_total FROM invoices WHERE id='.(int)$invoice_id)->fetch_assoc();
      if($inv && $tot >= (float)$inv['grand_total']){
        $mysqli->query('UPDATE invoices SET status="paid" WHERE id='.(int)$invoice_id);
        // Update items from reserved to sold
        $mysqli->query('UPDATE final_products fp JOIN invoice_items ii ON ii.final_product_id=fp.id SET fp.sale_status="sold" WHERE ii.invoice_id='.(int)$invoice_id.' AND fp.sale_status="reserved"');
        $mysqli->query('UPDATE production_items pi JOIN invoice_items ii ON ii.production_item_id=pi.id SET pi.availability="sold" WHERE ii.invoice_id='.(int)$invoice_id.' AND pi.availability="reserved"');
      }
      // Record in cash book - Receipt entry for advance
      require_once __DIR__ . '/../includes/finance_service.php';
      $submittedBankId = intval($_POST['bank_account_id'] ?? 0);
      $bankAccountId = $submittedBankId ?: getDefaultBankAccountId();
      recordIncomePayment(
          $invoice_id,
          $bankAccountId,
          $advance_amount,
          [
              'transaction_date' => $pay_date,
              'narration' => 'Advance payment for invoice - ' . $invoice_no,
              'document_reference' => $ref . ' - ' . $invoice_no,
              'account_head' => 'Sales Revenue',
              'entered_by' => $_SESSION['user_id'] ?? null
          ]
      );
    }

    json_response(['message'=>'Created','invoice_id'=>$invoice_id,'invoice_no'=>$invoice_no]);
    break;

  case 'update':
    ensure_admin_invoice();
    if (!validate_csrf($_POST['csrf'] ?? '')) json_response(['message'=>'Invalid CSRF'], 400);
    $id = (int)($_POST['id'] ?? 0); if(!$id) json_response(['message'=>'Missing id'],400);
    // Load invoice status and branch
    $inv = $mysqli->query('SELECT status, branch_id FROM invoices WHERE id='.(int)$id)->fetch_assoc();
    if(!$inv) json_response(['message'=>'Not found'],404);
    if (!is_admin_user()){
      $bid=(int)(sess_branch_id() ?? 0); if ((int)($inv['branch_id'] ?? 0) !== $bid) json_response(['message'=>'Forbidden'],403);
    }
    $statusCur = $inv['status'];
    if ($statusCur !== 'draft'){
      // require an approved amend request
      $chk = $mysqli->query('SELECT id FROM invoice_approvals WHERE invoice_id='.(int)$id." AND action='amend' AND status='approved' AND used=0 ORDER BY id DESC LIMIT 1");
      $appr = $chk ? $chk->fetch_assoc() : null;
      if(!$appr) json_response(['message'=>'Amend not approved'],403);
    }
    $items = json_decode($_POST['items_json'] ?? '[]', true) ?: [];
    if (empty($items)) json_response(['message'=>'At least one item required'],400);
    $customer_id = (int)($_POST['customer_id'] ?? 0);
    if (!$customer_id) json_response(['message'=>'Invalid customer'],400);
    $invoice_date = $_POST['invoice_date'] ?? date('Y-m-d');
    $payment_term = trim($_POST['payment_term'] ?? '');
    $new_status = trim($_POST['status'] ?? 'draft');
    if (!in_array($new_status, ['draft','issued'], true)) json_response(['message'=>'Invalid status'],400);
    $tax_percent = (float)($_POST['tax_percent'] ?? 0);
    $discount_amount_input = $_POST['discount'] ?? '';
    $discount_percent_input = $_POST['discount_percent'] ?? '';
    $transport_charges = (float)($_POST['transport_charges'] ?? 0);
    $notes = trim($_POST['notes'] ?? '');

    $mysqli->begin_transaction();
    try{
      // release previous reservations
      $resOld = $mysqli->query('SELECT id, final_product_id, cut_piece_id, production_item_id FROM invoice_items WHERE invoice_id='.(int)$id);
      while($resOld && ($r=$resOld->fetch_assoc())){
        if ($r['final_product_id']) { $mysqli->query('UPDATE final_products SET sale_status="available" WHERE id='.(int)$r['final_product_id']); }
        if ($r['cut_piece_id']) { $mysqli->query('UPDATE cut_pieces SET is_available=1, reserved_invoice_item_id=NULL WHERE id='.(int)$r['cut_piece_id']); }
        if ($r['production_item_id']) { $mysqli->query('UPDATE production_items SET availability="available" WHERE id='.(int)$r['production_item_id']); }
      }
      // delete old items
      $mysqli->query('DELETE FROM invoice_items WHERE invoice_id='.(int)$id);
      // recompute totals and insert new
      $subtotal=0.0; foreach($items as $it){ $subtotal += (float)($it['qty']??1) * (float)($it['unit_price']??0); }
      $tax_amount = round($subtotal * $tax_percent / 100, 2);
      $discount_percent = ($discount_percent_input !== '') ? (float)$discount_percent_input : null;
      if ($discount_percent !== null) { $discount = round($subtotal * $discount_percent / 100, 2); } else { $discount = ($discount_amount_input !== '') ? round((float)$discount_amount_input, 2) : 0.0; }
      $grand_total = max(0, round($subtotal + $tax_amount + $transport_charges - $discount, 2));

      $up = $mysqli->prepare('UPDATE invoices SET customer_id=?, invoice_date=?, status=?, payment_term=?, subtotal=?, tax_percent=?, tax_amount=?, transport_charges=?, discount_percent=?, discount=?, grand_total=?, notes=? WHERE id=?');
      $up->bind_param('isssdddddddsi', $customer_id, $invoice_date, $new_status, $payment_term, $subtotal, $tax_percent, $tax_amount, $transport_charges, $discount_percent, $discount, $grand_total, $notes, $id);
      if(!$up->execute()) throw new Exception($up->error);

      $ins = $mysqli->prepare('INSERT INTO invoice_items (invoice_id, final_product_id, cut_piece_id, production_item_id, service_id, description, qty, length_ft, returned_qty, unit_price, line_total) VALUES (?,?,?,?,?,?,?,?,?,?,?)');
      foreach ($items as $it){
        $fp_id = !empty($it['final_product_id']) ? (int)$it['final_product_id'] : null;
        $cp_id = !empty($it['cut_piece_id']) ? (int)$it['cut_piece_id'] : null;
        $pi_id = !empty($it['production_item_id']) ? (int)$it['production_item_id'] : null;
        $sv_id = !empty($it['service_id']) ? (int)$it['service_id'] : null;
        $desc = trim($it['description'] ?? 'Item');
        $length_ft = isset($it['length_ft']) ? (float)$it['length_ft'] : null;
        $qty = ($length_ft !== null) ? (float)$length_ft : (float)($it['qty'] ?? 1);
        $price = (float)($it['unit_price'] ?? 0);
        $total = round($qty * $price, 2);
        $ret = 0.0;
        $ins->bind_param('iiiiisdddds', $id, $fp_id, $cp_id, $pi_id, $sv_id, $desc, $qty, $length_ft, $ret, $price, $total);
        if(!$ins->execute()) throw new Exception($ins->error);
        $item_id = $mysqli->insert_id;
        if ($fp_id) { $mysqli->query('UPDATE final_products SET sale_status="reserved" WHERE id='.(int)$fp_id); }
        if ($cp_id) { $mysqli->query('UPDATE cut_pieces SET is_available=0, reserved_invoice_item_id='.(int)$item_id.' WHERE id='.(int)$cp_id); }
        if ($pi_id) { $mysqli->query('UPDATE production_items SET availability="reserved" WHERE id='.(int)$pi_id); }
      }

      // If moved to issued, apply issue-side effects (mark reserved items as sold/consumed)
      if ($new_status === 'issued') {
        // mark final products as sold
        $res=$mysqli->query('SELECT final_product_id FROM invoice_items WHERE invoice_id='.(int)$id.' AND final_product_id IS NOT NULL');
        while($res && ($r=$res->fetch_assoc())){ $mysqli->query('UPDATE final_products SET sale_status="sold" WHERE id='.(int)$r['final_product_id']); }
        // consume cut pieces and log inventory move
        $cut = $mysqli->query('SELECT it.cut_piece_id, cp.area_sq_ft FROM invoice_items it JOIN cut_pieces cp ON cp.id=it.cut_piece_id WHERE it.invoice_id='.(int)$id.' AND it.cut_piece_id IS NOT NULL');
        $loc = $mysqli->query("SELECT id FROM store_locations WHERE name='Cut Slab Warehouse' LIMIT 1");
        $cutLoc = ($loc && ($x=$loc->fetch_assoc())) ? (int)$x['id'] : null;
        while($cut && ($c=$cut->fetch_assoc())){
          $cpid = (int)$c['cut_piece_id'];
          $qty = (float)$c['area_sq_ft'];
          $mysqli->query('UPDATE cut_pieces SET is_available=0 WHERE id='.(int)$cpid);
          if ($cutLoc) {
            $mv = $mysqli->prepare("INSERT INTO inventory_moves (from_location_id, to_location_id, ref_type, ref_id, quantity, unit, notes) VALUES (?,?, 'invoice', ?, ?, 'sqft', 'Cut piece sold')");
            $to = null; $refId = $id;
            $mv->bind_param('iidi', $cutLoc, $to, $refId, $qty);
            @$mv->execute();
          }
        }
        // mark production items as sold
        $pi = $mysqli->query('SELECT production_item_id FROM invoice_items WHERE invoice_id='.(int)$id.' AND production_item_id IS NOT NULL');
        while($pi && ($p=$pi->fetch_assoc())){ $mysqli->query('UPDATE production_items SET availability="sold" WHERE id='.(int)$p['production_item_id']); }
      }

      // mark amendment approval as used
      if (isset($appr['id'])) { $mysqli->query('UPDATE invoice_approvals SET used=1, decided_at=NOW() WHERE id='.(int)$appr['id']); }
      $mysqli->commit();
      json_response(['message'=>'Updated']);
    } catch (Exception $e){ $mysqli->rollback(); json_response(['message'=>$e->getMessage()],500); }
    break;

  case 'issue':
    ensure_admin_invoice();
    if (!validate_csrf($_POST['csrf'] ?? '')) json_response(['message'=>'Invalid CSRF'], 400);
    $id=(int)($_POST['id'] ?? 0); if(!$id) json_response(['message'=>'Missing id'],400);
    if (!is_admin_user()){
      $chk=$mysqli->query('SELECT branch_id FROM invoices WHERE id='.(int)$id)->fetch_assoc(); if(!$chk) json_response(['message'=>'Not found'],404);
      $bid=(int)(sess_branch_id() ?? 0); if ((int)($chk['branch_id'] ?? 0) !== $bid) json_response(['message'=>'Forbidden'],403);
    }
    $mysqli->query('UPDATE invoices SET status="issued" WHERE id='.(int)$id);
    // mark reserved items as sold on issue (or on full payment—adjust later if needed)
    $res=$mysqli->query('SELECT final_product_id FROM invoice_items WHERE invoice_id='.(int)$id.' AND final_product_id IS NOT NULL');
    while($r=$res->fetch_assoc()){ $mysqli->query('UPDATE final_products SET sale_status="sold" WHERE id='.(int)$r['final_product_id']); }
    // consume cut pieces on issue and log inventory move
    $cut = $mysqli->query('SELECT it.cut_piece_id, cp.area_sq_ft FROM invoice_items it JOIN cut_pieces cp ON cp.id=it.cut_piece_id WHERE it.invoice_id='.(int)$id.' AND it.cut_piece_id IS NOT NULL');
    // find Cut Slab Warehouse id
    $loc = $mysqli->query("SELECT id FROM store_locations WHERE name='Cut Slab Warehouse' LIMIT 1");
    $cutLoc = ($loc && ($x=$loc->fetch_assoc())) ? (int)$x['id'] : null;
    while($cut && ($c=$cut->fetch_assoc())){
      $cpid = (int)$c['cut_piece_id'];
      $qty = (float)$c['area_sq_ft'];
      $mysqli->query('UPDATE cut_pieces SET is_available=0 WHERE id='.(int)$cpid);
      if ($cutLoc) {
        $mv = $mysqli->prepare("INSERT INTO inventory_moves (from_location_id, to_location_id, ref_type, ref_id, quantity, unit, notes) VALUES (?,?, 'invoice', ?, ?, 'sqft', 'Cut piece sold')");
        $to = null; $refId = $id;
        $mv->bind_param('iidi', $cutLoc, $to, $refId, $qty);
        @$mv->execute();
      }
    }
    // mark production items as sold
    $pi = $mysqli->query('SELECT production_item_id FROM invoice_items WHERE invoice_id='.(int)$id.' AND production_item_id IS NOT NULL');
    while($pi && ($p=$pi->fetch_assoc())){
      $mysqli->query('UPDATE production_items SET availability="sold" WHERE id='.(int)$p['production_item_id']);
    }
    json_response(['message'=>'Issued']);
    break;

  case 'request_cancel':
    ensure_admin_invoice();
    if (!validate_csrf($_POST['csrf'] ?? '')) json_response(['message'=>'Invalid CSRF'], 400);
    $invoice_id=(int)($_POST['invoice_id'] ?? 0); if(!$invoice_id) json_response(['message'=>'Missing invoice_id'],400);
    $reason=trim($_POST['reason'] ?? '');
    $stmt=$mysqli->prepare("INSERT INTO invoice_approvals (invoice_id, action, reason, status) VALUES (?,?,?, 'pending')");
    $act='cancel'; $stmt->bind_param('iss',$invoice_id,$act,$reason); @$stmt->execute();
    json_response(['message'=>'Cancel requested']);
    break;

  case 'approve_cancel':
    ensure_admin_invoice();
    if (!validate_csrf($_POST['csrf'] ?? '')) json_response(['message'=>'Invalid CSRF'], 400);
    $invoice_id=(int)($_POST['invoice_id'] ?? 0); if(!$invoice_id) json_response(['message'=>'Missing invoice_id'],400);
    $mysqli->begin_transaction();
    try{
      // perform same as cancel
      $mysqli->query('UPDATE invoices SET status="cancelled" WHERE id='.(int)$invoice_id);
      $res=$mysqli->query('SELECT final_product_id FROM invoice_items WHERE invoice_id='.(int)$invoice_id.' AND final_product_id IS NOT NULL');
      while($r=$res->fetch_assoc()){ $mysqli->query('UPDATE final_products SET sale_status="available" WHERE id='.(int)$r['final_product_id']); }
      $mysqli->query('UPDATE cut_pieces cp JOIN invoice_items it ON it.cut_piece_id=cp.id SET cp.is_available=1, cp.reserved_invoice_item_id=NULL WHERE it.invoice_id='.(int)$invoice_id.' AND it.cut_piece_id IS NOT NULL');
      $mysqli->query('UPDATE production_items pi JOIN invoice_items it ON it.production_item_id=pi.id SET pi.availability="available" WHERE it.invoice_id='.(int)$invoice_id.' AND it.production_item_id IS NOT NULL');
      // mark latest pending cancel approval as approved
      $mysqli->query('UPDATE invoice_approvals SET status="approved", decided_at=NOW() WHERE invoice_id='.(int)$invoice_id." AND action='cancel' AND status='pending' ORDER BY id DESC LIMIT 1");
      $mysqli->commit(); json_response(['message'=>'Cancelled']);
    } catch (Exception $e){ $mysqli->rollback(); json_response(['message'=>$e->getMessage()],500); }
    break;

  case 'request_amend':
    ensure_admin_invoice();
    if (!validate_csrf($_POST['csrf'] ?? '')) json_response(['message'=>'Invalid CSRF'], 400);
    $invoice_id=(int)($_POST['invoice_id'] ?? 0); if(!$invoice_id) json_response(['message'=>'Missing invoice_id'],400);
    $reason=trim($_POST['reason'] ?? '');
    $stmt=$mysqli->prepare("INSERT INTO invoice_approvals (invoice_id, action, reason, status) VALUES (?,?,?, 'pending')");
    $act='amend'; $stmt->bind_param('iss',$invoice_id,$act,$reason); @$stmt->execute();
    json_response(['message'=>'Amend requested']);
    break;

  case 'approve_amend':
    ensure_admin_invoice();
    if (!validate_csrf($_POST['csrf'] ?? '')) json_response(['message'=>'Invalid CSRF'], 400);
    $invoice_id=(int)($_POST['invoice_id'] ?? 0); if(!$invoice_id) json_response(['message'=>'Missing invoice_id'],400);
    $mysqli->query('UPDATE invoice_approvals SET status="approved", decided_at=NOW() WHERE invoice_id='.(int)$invoice_id." AND action='amend' AND status='pending' ORDER BY id DESC LIMIT 1");
    json_response(['message'=>'Amend approved']);
    break;

  case 'return_item':
    ensure_admin_invoice();
    if (!validate_csrf($_POST['csrf'] ?? '')) json_response(['message'=>'Invalid CSRF'], 400);
    // accept both legacy and new param names
    $invoice_item_id=(int)($_POST['invoice_item_id'] ?? ($_POST['item_id'] ?? 0)); if(!$invoice_item_id) json_response(['message'=>'Missing invoice_item_id'],400);
    $qty_ret=(float)($_POST['qty_returned'] ?? ($_POST['qty'] ?? 0)); if($qty_ret<=0) json_response(['message'=>'Invalid qty_returned'],400);
    $reason=trim($_POST['reason'] ?? 'Return');
    $row=$mysqli->query('SELECT * FROM invoice_items WHERE id='.(int)$invoice_item_id)->fetch_assoc(); if(!$row) json_response(['message'=>'Item not found'],404);
    // validate not exceeding remaining quantity
    $remaining = max(0, (float)$row['qty'] - (float)$row['returned_qty']);
    if ($qty_ret > $remaining) json_response(['message'=>'Return quantity exceeds remaining quantity'],400);
    $mysqli->begin_transaction();
    try{
      // log return
      $r=$mysqli->prepare('INSERT INTO invoice_return_items (invoice_item_id, qty_returned, reason) VALUES (?,?,?)');
      $r->bind_param('ids',$invoice_item_id,$qty_ret,$reason); @$r->execute();
      // update returned qty and line total
      $newReturned = (float)$row['returned_qty'] + $qty_ret;
      $newQty = max(0, (float)$row['qty'] - $newReturned);
      $newLine = round($newQty * (float)$row['unit_price'], 2);
      $u=$mysqli->prepare('UPDATE invoice_items SET returned_qty=?, line_total=? WHERE id=?');
      $u->bind_param('ddi',$newReturned,$newLine,$invoice_item_id); @$u->execute();
      // adjust inventory back for this item
      if (!empty($row['final_product_id'])){ $mysqli->query('UPDATE final_products SET sale_status="available" WHERE id='.(int)$row['final_product_id']); }
      if (!empty($row['cut_piece_id'])){ $mysqli->query('UPDATE cut_pieces SET is_available=1, reserved_invoice_item_id=NULL WHERE id='.(int)$row['cut_piece_id']); }
      if (!empty($row['production_item_id'])){ $mysqli->query('UPDATE production_items SET availability="available" WHERE id='.(int)$row['production_item_id']); }
      // recompute invoice totals
      $idInv=(int)$row['invoice_id'];
      $tot=$mysqli->query('SELECT SUM(line_total) s FROM invoice_items WHERE invoice_id='.(int)$idInv)->fetch_assoc();
      $subtotal=(float)$tot['s'];
      $inv=$mysqli->query('SELECT tax_percent, transport_charges, discount_percent, discount FROM invoices WHERE id='.(int)$idInv)->fetch_assoc();
      $tax_amount = round($subtotal * (float)$inv['tax_percent'] / 100, 2);
      $discount = ($inv['discount_percent']!==null) ? round($subtotal * (float)$inv['discount_percent'] / 100, 2) : (float)$inv['discount'];
      $grand_total = max(0, round($subtotal + $tax_amount + (float)$inv['transport_charges'] - $discount, 2));
      $uu=$mysqli->prepare('UPDATE invoices SET subtotal=?, tax_amount=?, grand_total=? WHERE id=?');
      $uu->bind_param('dddi',$subtotal,$tax_amount,$grand_total,$idInv); @$uu->execute();
      // adjust payments if overpaid due to return
      $sum=$mysqli->query('SELECT COALESCE(SUM(amount),0) s FROM payments WHERE invoice_id='.(int)$idInv)->fetch_assoc();
      $paid=(float)$sum['s'];
      if ($paid > $grand_total) {
        $over = round($paid - $grand_total, 2);
        if ($over > 0) {
          $pp=$mysqli->prepare('INSERT INTO payments (invoice_id, pay_date, method, amount, reference_no, notes) VALUES (?,?,?,?,?,?)');
          $today=date('Y-m-d'); $m='Adjustment'; $ref='Return adjustment'; $note='Auto adjustment for item return';
          $neg = -$over; $pp->bind_param('issdss', $idInv, $today, $m, $neg, $ref, $note); @$pp->execute();
          // recalc paid after adjustment
          $sum2=$mysqli->query('SELECT COALESCE(SUM(amount),0) s FROM payments WHERE invoice_id='.(int)$idInv)->fetch_assoc();
          $paid = (float)$sum2['s'];
        }
      }
      // set invoice status according to new paid vs total
      if ($paid >= $grand_total) {
        $mysqli->query('UPDATE invoices SET status="paid" WHERE id='.(int)$idInv);
        // Update items from reserved to sold
        $mysqli->query('UPDATE final_products fp JOIN invoice_items ii ON ii.final_product_id=fp.id SET fp.sale_status="sold" WHERE ii.invoice_id='.(int)$idInv.' AND fp.sale_status="reserved"');
        $mysqli->query('UPDATE production_items pi JOIN invoice_items ii ON ii.production_item_id=pi.id SET pi.availability="sold" WHERE ii.invoice_id='.(int)$idInv.' AND pi.availability="reserved"');
      } else {
        $mysqli->query('UPDATE invoices SET status="issued" WHERE id='.(int)$idInv);
      }
      $mysqli->commit(); json_response(['message'=>'Item returned']);
    } catch (Exception $e){ $mysqli->rollback(); json_response(['message'=>$e->getMessage()],500); }
    break;

  case 'cancel':
    ensure_admin_invoice();
    if (!validate_csrf($_POST['csrf'] ?? '')) json_response(['message'=>'Invalid CSRF'], 400);
    $id=(int)($_POST['id'] ?? 0); if(!$id) json_response(['message'=>'Missing id'],400);
    if (!is_admin_user()){
      $chk=$mysqli->query('SELECT branch_id FROM invoices WHERE id='.(int)$id)->fetch_assoc(); if(!$chk) json_response(['message'=>'Not found'],404);
      $bid=(int)(sess_branch_id() ?? 0); if ((int)($chk['branch_id'] ?? 0) !== $bid) json_response(['message'=>'Forbidden'],403);
    }
    // set status cancelled
    $mysqli->query('UPDATE invoices SET status="cancelled" WHERE id='.(int)$id);
    // unreserve final products
    $res=$mysqli->query('SELECT final_product_id FROM invoice_items WHERE invoice_id='.(int)$id.' AND final_product_id IS NOT NULL');
    while($r=$res->fetch_assoc()){ $mysqli->query('UPDATE final_products SET sale_status="available" WHERE id='.(int)$r['final_product_id']); }
    // unreserve cut pieces
    $mysqli->query('UPDATE cut_pieces cp JOIN invoice_items it ON it.cut_piece_id=cp.id SET cp.is_available=1, cp.reserved_invoice_item_id=NULL WHERE it.invoice_id='.(int)$id.' AND it.cut_piece_id IS NOT NULL');
    // unreserve production items
    $mysqli->query('UPDATE production_items pi JOIN invoice_items it ON it.production_item_id=pi.id SET pi.availability="available" WHERE it.invoice_id='.(int)$id.' AND it.production_item_id IS NOT NULL');
    json_response(['message'=>'Cancelled']);
    break;

  case 'add_payment':
    ensure_admin_invoice();
    if (!validate_csrf($_POST['csrf'] ?? '')) json_response(['message'=>'Invalid CSRF'], 400);
    $invoice_id=(int)($_POST['invoice_id'] ?? 0);
    $amount=(float)($_POST['amount'] ?? 0);
    $pay_date=$_POST['pay_date'] ?? date('Y-m-d');
    $method=trim($_POST['method'] ?? 'Cash');
    $ref=trim($_POST['reference_no'] ?? '');
    $notes=trim($_POST['notes'] ?? '');
    if(!$invoice_id || $amount<=0) json_response(['message'=>'Invalid input'],400);
    // ensure same-branch payment
    if (!is_admin_user()){
      $chk=$mysqli->query('SELECT branch_id FROM invoices WHERE id='.(int)$invoice_id)->fetch_assoc(); if(!$chk) json_response(['message'=>'Invoice not found'],404);
      $bid=(int)(sess_branch_id() ?? 0); if ((int)($chk['branch_id'] ?? 0) !== $bid) json_response(['message'=>'Forbidden'],403);
    }
    $stmt=$mysqli->prepare('INSERT INTO payments (invoice_id, pay_date, method, amount, reference_no, notes) VALUES (?,?,?,?,?,?)');
    $stmt->bind_param('issdss',$invoice_id,$pay_date,$method,$amount,$ref,$notes);
    if(!$stmt->execute()) json_response(['message'=>$stmt->error],500);
    $paymentId = $stmt->insert_id;
    $stmt->close();
    // If total payments >= grand_total, mark paid
    $sum=$mysqli->query('SELECT COALESCE(SUM(amount),0) s FROM payments WHERE invoice_id='.(int)$invoice_id)->fetch_assoc();
    $tot=(float)$sum['s'];
    $inv=$mysqli->query('SELECT grand_total, invoice_no FROM invoices WHERE id='.(int)$invoice_id)->fetch_assoc();
    if($inv && $tot >= (float)$inv['grand_total']){
      $mysqli->query('UPDATE invoices SET status="paid" WHERE id='.(int)$invoice_id);
      // Update items from reserved to sold
      $mysqli->query('UPDATE final_products fp JOIN invoice_items ii ON ii.final_product_id=fp.id SET fp.sale_status="sold" WHERE ii.invoice_id='.(int)$invoice_id.' AND fp.sale_status="reserved"');
      $mysqli->query('UPDATE production_items pi JOIN invoice_items ii ON ii.production_item_id=pi.id SET pi.availability="sold" WHERE ii.invoice_id='.(int)$invoice_id.' AND pi.availability="reserved"');
    }
    // Record in cash book - Receipt entry
    require_once __DIR__ . '/../includes/finance_service.php';
    $cashBookRef = 'INV-' . $invoice_id . ($inv ? ' (' . $inv['invoice_no'] . ')' : '');
    $submittedBankId = intval($_POST['bank_account_id'] ?? 0);
    $bankAccountId = $submittedBankId ?: getDefaultBankAccountId();
    $financeService = new FinanceService();
    $result = $financeService->recordIncome(
        $invoice_id,
        $bankAccountId,
        $amount,
        [
            'transaction_date' => $pay_date,
            'narration' => 'Invoice payment received - ' . ($inv ? $inv['invoice_no'] : $invoice_id) . ($notes ? ' - ' . $notes : ''),
            'document_reference' => $ref ?: $cashBookRef,
            'account_head' => 'Sales Revenue',
            'entered_by' => $_SESSION['user']['id'] ?? null
        ]
    );
    // Ensure transaction_id column exists (safe to run every time - no-op if already present)
    $mysqli->query('ALTER TABLE payments ADD COLUMN IF NOT EXISTS transaction_id INT DEFAULT NULL');
    // Store transaction_id in payment record for traceability
    if ($result['success'] && $result['transaction_id']) {
        $updStmt = $mysqli->prepare('UPDATE payments SET transaction_id = ? WHERE id = ?');
        $updStmt->bind_param('ii', $result['transaction_id'], $paymentId);
        $updStmt->execute();
        $updStmt->close();
    }
    json_response(['message'=>'Payment added and recorded in cash book', 'transaction_id' => $result['transaction_id'] ?? null]);
    break;

  case 'update_payment':
    if (!validate_csrf($_POST['csrf'] ?? '')) json_response(['message'=>'Invalid CSRF'], 400);
    $id=(int)($_POST['id'] ?? 0); if(!$id) json_response(['message'=>'Missing payment id'],400);
    $row=$mysqli->query('SELECT * FROM payments WHERE id='.(int)$id)->fetch_assoc(); if(!$row) json_response(['message'=>'Payment not found'],404);
    $invoice_id=(int)$row['invoice_id'];
    if (!is_admin_user()){
      $chk=$mysqli->query('SELECT branch_id FROM invoices WHERE id='.(int)$invoice_id)->fetch_assoc(); if(!$chk) json_response(['message'=>'Invoice not found'],404);
      $bid=(int)(sess_branch_id() ?? 0); if ((int)($chk['branch_id'] ?? 0) !== $bid) json_response(['message'=>'Forbidden'],403);
    }
    $oldAmount = (float)$row['amount'];
    $pay_date = $_POST['pay_date'] ?? $row['pay_date'];
    $method = trim($_POST['method'] ?? $row['method']);
    $amount = (float)($_POST['amount'] ?? $row['amount']); if ($amount<=0) json_response(['message'=>'Invalid amount'],400);
    $ref = trim($_POST['reference_no'] ?? $row['reference_no']);
    $notes = trim($_POST['notes'] ?? $row['notes']);
    
    $mysqli->begin_transaction();
    try {
        // Update payment record
        $stmt=$mysqli->prepare('UPDATE payments SET pay_date=?, method=?, amount=?, reference_no=?, notes=? WHERE id=?');
        $stmt->bind_param('ssdssi',$pay_date,$method,$amount,$ref,$notes,$id);
        if(!$stmt->execute()) throw new Exception($stmt->error);
        $stmt->close();
        
        // Update related cash book transaction if exists
        $transId = $row['transaction_id'] ?? null;
        
        // Fallback: find transaction by document reference for old payments
        if (!$transId) {
            // Use original reference from payment record to find the transaction
            $findStmt = $mysqli->prepare("SELECT id FROM transactions WHERE transaction_type = 'Receipt' AND (document_reference = ? OR document_reference LIKE ?) LIMIT 1");
            $originalRef = $row['reference_no'] ?: 'INV-' . $invoice_id;
            $likePattern = 'INV-' . $invoice_id . '%';
            $findStmt->bind_param('ss', $originalRef, $likePattern);
            $findStmt->execute();
            $findResult = $findStmt->get_result();
            if ($findRow = $findResult->fetch_assoc()) {
                $transId = $findRow['id'];
                // Store the found transaction_id for future use
                $updLinkStmt = $mysqli->prepare('UPDATE payments SET transaction_id = ? WHERE id = ?');
                $updLinkStmt->bind_param('ii', $transId, $id);
                $updLinkStmt->execute();
                $updLinkStmt->close();
            }
            $findStmt->close();
        }
        
        if ($transId) {
            require_once __DIR__ . '/../includes/finance_service.php';
            $amountDiff = $amount - $oldAmount;
            
            // Get current transaction details
            $trans = $mysqli->query('SELECT * FROM transactions WHERE id='.(int)$transId)->fetch_assoc();
            if ($trans) {
                $newTransAmount = $amount;
                $newAmountInWords = amount_in_words($newTransAmount, 'LKR');
                
                // Update the transaction (always update even if amount same, for date/ref changes)
                $updTrans = $mysqli->prepare('UPDATE transactions SET 
                    amount = ?, 
                    amount_in_words = ?,
                    transaction_date = ?,
                    narration = ?,
                    document_reference = ?
                    WHERE id = ?');
                $narration = 'Invoice payment received - INV-' . $invoice_id . ($notes ? ' - ' . $notes : '');
                $docRef = $ref ?: 'INV-' . $invoice_id;
                $updTrans->bind_param('dssssi', $newTransAmount, $newAmountInWords, $pay_date, $narration, $docRef, $transId);
                $updTrans->execute();
                $updTrans->close();
                
                // Adjust bank balance by the difference (only if amount changed)
                if ($amountDiff != 0) {
                    $bankId = $trans['bank_account_id'];
                    if ($bankId) {
                        $balStmt = $mysqli->prepare('UPDATE bank_accounts SET current_balance = current_balance + ? WHERE id = ?');
                        $balStmt->bind_param('di', $amountDiff, $bankId);
                        $balStmt->execute();
                        $balStmt->close();
                    }
                }
            }
        }
        
        // recompute invoice paid/ status
        $sum=$mysqli->query('SELECT COALESCE(SUM(amount),0) s FROM payments WHERE invoice_id='.(int)$invoice_id)->fetch_assoc();
        $tot=(float)$sum['s'];
        $inv=$mysqli->query('SELECT grand_total FROM invoices WHERE id='.(int)$invoice_id)->fetch_assoc();
        if($inv){
          if ($tot >= (float)$inv['grand_total']) {
            $mysqli->query('UPDATE invoices SET status="paid" WHERE id='.(int)$invoice_id);
            // Update items from reserved to sold
            $mysqli->query('UPDATE final_products fp JOIN invoice_items ii ON ii.final_product_id=fp.id SET fp.sale_status="sold" WHERE ii.invoice_id='.(int)$invoice_id.' AND fp.sale_status="reserved"');
            $mysqli->query('UPDATE production_items pi JOIN invoice_items ii ON ii.production_item_id=pi.id SET pi.availability="sold" WHERE ii.invoice_id='.(int)$invoice_id.' AND pi.availability="reserved"');
          }
          else if ($tot < (float)$inv['grand_total']) $mysqli->query('UPDATE invoices SET status="issued" WHERE id='.(int)$invoice_id);
        }
        
        $mysqli->commit();
        json_response(['message'=>'Payment updated and cash book adjusted']);
    } catch (Exception $e) {
        $mysqli->rollback();
        json_response(['message'=>'Update failed: ' . $e->getMessage()], 500);
    }
    break;

  case 'delete_payment':
    if (!validate_csrf($_POST['csrf'] ?? '')) json_response(['message'=>'Invalid CSRF'], 400);
    $id=(int)($_POST['id'] ?? 0); if(!$id) json_response(['message'=>'Missing payment id'],400);
    $row=$mysqli->query('SELECT * FROM payments WHERE id='.(int)$id)->fetch_assoc(); if(!$row) json_response(['message'=>'Payment not found'],404);
    $invoice_id=(int)$row['invoice_id'];
    if (!is_admin_user()){
      $chk=$mysqli->query('SELECT branch_id FROM invoices WHERE id='.(int)$invoice_id)->fetch_assoc(); if(!$chk) json_response(['message'=>'Invoice not found'],404);
      $bid=(int)(sess_branch_id() ?? 0); if ((int)($chk['branch_id'] ?? 0) !== $bid) json_response(['message'=>'Forbidden'],403);
    }
    
    $mysqli->begin_transaction();
    $transId = null;
    try {
        // Find transaction_id if not set (for old payments)
        $transId = $row['transaction_id'] ?? null;
        if (!$transId) {
            // Try multiple search patterns to find the transaction
            $searchPatterns = [
                $row['reference_no'],                           // Exact payment reference
                'INV-' . $invoice_id,                         // Invoice ID pattern
                'INV-' . $invoice_id . ' %',                  // With invoice number suffix
            ];
            
            foreach ($searchPatterns as $pattern) {
                if (!$pattern) continue;
                $findStmt = $mysqli->prepare("SELECT id FROM transactions WHERE transaction_type = 'Receipt' AND (document_reference = ? OR document_reference LIKE ? OR narration LIKE ?) LIMIT 1");
                $likePattern = $pattern . '%';
                $narrationPattern = '%INV-' . $invoice_id . '%';
                $findStmt->bind_param('sss', $pattern, $likePattern, $narrationPattern);
                $findStmt->execute();
                $findResult = $findStmt->get_result();
                if ($findRow = $findResult->fetch_assoc()) {
                    $transId = $findRow['id'];
                    $findStmt->close();
                    break;
                }
                $findStmt->close();
            }
        }
        
        // Reverse cash book entry if exists
        if ($transId) {
            require_once __DIR__ . '/../includes/finance_service.php';
            $trans = $mysqli->query('SELECT * FROM transactions WHERE id='.(int)$transId)->fetch_assoc();
            if ($trans) {
                // Reverse bank balance
                $bankId = $trans['bank_account_id'];
                $amount = (float)$trans['amount'];
                if ($bankId) {
                    $balStmt = $mysqli->prepare('UPDATE bank_accounts SET current_balance = current_balance - ? WHERE id = ?');
                    $balStmt->bind_param('di', $amount, $bankId);
                    $balStmt->execute();
                    $balStmt->close();
                }
                
                // Cancel the transaction
                $cancelStmt = $mysqli->prepare('UPDATE transactions SET is_cancelled = 1, cancellation_reason = ? WHERE id = ?');
                $reason = 'Payment deleted - Invoice #' . $invoice_id;
                $cancelStmt->bind_param('si', $reason, $transId);
                $cancelStmt->execute();
                $cancelStmt->close();
            }
        }
        
        // Delete payment record
        $mysqli->query('DELETE FROM payments WHERE id='.(int)$id);
        
        // recompute invoice paid/status
        $sum=$mysqli->query('SELECT COALESCE(SUM(amount),0) s FROM payments WHERE invoice_id='.(int)$invoice_id)->fetch_assoc();
        $tot=(float)$sum['s'];
        $inv=$mysqli->query('SELECT grand_total FROM invoices WHERE id='.(int)$invoice_id)->fetch_assoc();
        if($inv){
          if ($tot >= (float)$inv['grand_total']) {
            $mysqli->query('UPDATE invoices SET status="paid" WHERE id='.(int)$invoice_id);
            // Update items from reserved to sold
            $mysqli->query('UPDATE final_products fp JOIN invoice_items ii ON ii.final_product_id=fp.id SET fp.sale_status="sold" WHERE ii.invoice_id='.(int)$invoice_id.' AND fp.sale_status="reserved"');
            $mysqli->query('UPDATE production_items pi JOIN invoice_items ii ON ii.production_item_id=pi.id SET pi.availability="sold" WHERE ii.invoice_id='.(int)$invoice_id.' AND pi.availability="reserved"');
          }
          else if ($tot < (float)$inv['grand_total']) $mysqli->query('UPDATE invoices SET status="issued" WHERE id='.(int)$invoice_id);
        }
        
        $mysqli->commit();
        $msg = $transId ? 'Payment deleted and cash book reversed' : 'Payment deleted (cash book entry not found)';
        json_response(['message' => $msg, 'transaction_found' => ($transId ? true : false)]);
    } catch (Exception $e) {
        $mysqli->rollback();
        json_response(['message'=>'Delete failed: ' . $e->getMessage()], 500);
    }
    break;

  case 'reverse_payment':
    if (!validate_csrf($_POST['csrf'] ?? '')) json_response(['message'=>'Invalid CSRF'], 400);
    $id=(int)($_POST['id'] ?? 0); if(!$id) json_response(['message'=>'Missing payment id'],400);
    $reason=trim($_POST['reason'] ?? '');
    $row=$mysqli->query('SELECT * FROM payments WHERE id='.(int)$id)->fetch_assoc(); if(!$row) json_response(['message'=>'Payment not found'],404);
    if((int)$row['is_reversed']===1) json_response(['message'=>'Payment already reversed'],400);
    $invoice_id=(int)$row['invoice_id'];
    if (!is_admin_user()){
      $chk=$mysqli->query('SELECT branch_id FROM invoices WHERE id='.(int)$invoice_id)->fetch_assoc(); if(!$chk) json_response(['message'=>'Invoice not found'],404);
      $bid=(int)(sess_branch_id() ?? 0); if ((int)($chk['branch_id'] ?? 0) !== $bid) json_response(['message'=>'Forbidden'],403);
    }
    
    $mysqli->begin_transaction();
    try {
        $amount=(float)$row['amount'];
        $revAmount = -$amount; // negative amount for reversal
        $pay_date=date('Y-m-d');
        $method='Reversal: '.$row['method'];
        $ref='REV-'.$row['reference_no'];
        $notes='Reversal of payment #'.$id.($reason?'. Reason: '.$reason:'');
        
        // Mark original payment as reversed
        $mysqli->query('UPDATE payments SET is_reversed=1 WHERE id='.(int)$id);
        
        // Create reversal payment with negative amount
        $stmt=$mysqli->prepare('INSERT INTO payments (invoice_id, pay_date, method, amount, reference_no, notes, payment_type, reversed_payment_id) VALUES (?,?,?,?,?,?,?,?)');
        $type='reversal';
        $stmt->bind_param('issdsssi',$invoice_id,$pay_date,$method,$revAmount,$ref,$notes,$type,$id);
        $stmt->execute();
        $reversalPaymentId = $stmt->insert_id;
        $stmt->close();
        
        // Find transaction_id if not set (for old payments)
        $transId = $row['transaction_id'] ?? null;
        if (!$transId) {
            $findStmt = $mysqli->prepare("SELECT id FROM transactions WHERE transaction_type = 'Receipt' AND (document_reference = ? OR document_reference LIKE ?) LIMIT 1");
            $originalRef = $row['reference_no'] ?: 'INV-' . $invoice_id;
            $likePattern = 'INV-' . $invoice_id . '%';
            $findStmt->bind_param('ss', $originalRef, $likePattern);
            $findStmt->execute();
            $findResult = $findStmt->get_result();
            if ($findRow = $findResult->fetch_assoc()) {
                $transId = $findRow['id'];
            }
            $findStmt->close();
        }
        
        // Reverse cash book entry if exists
        if ($transId) {
            require_once __DIR__ . '/../includes/finance_service.php';
            $trans = $mysqli->query('SELECT * FROM transactions WHERE id='.(int)$transId)->fetch_assoc();
            if ($trans) {
                $bankId = $trans['bank_account_id'];
                // Reverse bank balance (deduct the original amount since it's a receipt)
                if ($bankId) {
                    $balStmt = $mysqli->prepare('UPDATE bank_accounts SET current_balance = current_balance - ? WHERE id = ?');
                    $balStmt->bind_param('di', $amount, $bankId);
                    $balStmt->execute();
                    $balStmt->close();
                }
                
                // Cancel the original transaction
                $cancelStmt = $mysqli->prepare('UPDATE transactions SET is_cancelled = 1, cancellation_reason = ? WHERE id = ?');
                $cancelReason = 'Payment reversed - Reversal #' . $reversalPaymentId . ($reason ? ': ' . $reason : '');
                $cancelStmt->bind_param('si', $cancelReason, $transId);
                $cancelStmt->execute();
                $cancelStmt->close();
                
                // Create reversal transaction in cash book
                $financeService = new FinanceService();
                $invData = $mysqli->query('SELECT invoice_no FROM invoices WHERE id='.(int)$invoice_id)->fetch_assoc();
                $revResult = $financeService->recordIncome(
                    $invoice_id,
                    $bankId,
                    $revAmount, // Negative amount
                    [
                        'transaction_date' => $pay_date,
                        'narration' => 'Payment reversal - INV-' . $invoice_id . ($invData ? ' (' . $invData['invoice_no'] . ')' : '') . ($reason ? ' - ' . $reason : ''),
                        'document_reference' => $ref,
                        'account_head' => 'Sales Income Reversal',
                        'entered_by' => $_SESSION['user']['id'] ?? null
                    ]
                );
                
                // Link reversal payment to reversal transaction
                if ($revResult['success'] && $revResult['transaction_id']) {
                    $updStmt = $mysqli->prepare('UPDATE payments SET transaction_id = ? WHERE id = ?');
                    $updStmt->bind_param('ii', $revResult['transaction_id'], $reversalPaymentId);
                    $updStmt->execute();
                    $updStmt->close();
                }
            }
        }
        
        // recompute invoice paid/status
        $sum=$mysqli->query('SELECT COALESCE(SUM(amount),0) s FROM payments WHERE invoice_id='.(int)$invoice_id)->fetch_assoc();
        $tot=(float)$sum['s'];
        $inv=$mysqli->query('SELECT grand_total FROM invoices WHERE id='.(int)$invoice_id)->fetch_assoc();
        if($inv){
          if ($tot >= (float)$inv['grand_total']) {
            $mysqli->query('UPDATE invoices SET status="paid" WHERE id='.(int)$invoice_id);
            // Update items from reserved to sold
            $mysqli->query('UPDATE final_products fp JOIN invoice_items ii ON ii.final_product_id=fp.id SET fp.sale_status="sold" WHERE ii.invoice_id='.(int)$invoice_id.' AND fp.sale_status="reserved"');
            $mysqli->query('UPDATE production_items pi JOIN invoice_items ii ON ii.production_item_id=pi.id SET pi.availability="sold" WHERE ii.invoice_id='.(int)$invoice_id.' AND pi.availability="reserved"');
          }
          else if ($tot < (float)$inv['grand_total']) $mysqli->query('UPDATE invoices SET status="issued" WHERE id='.(int)$invoice_id);
        }
        
        $mysqli->commit();
        json_response(['message'=>'Payment reversed and cash book updated']);
    } catch (Exception $e) {
        $mysqli->rollback();
        json_response(['message'=>'Reversal failed: ' . $e->getMessage()], 500);
    }
    break;

  case 'get':
    $id=(int)($_GET['id'] ?? 0);
    $inv=$mysqli->query('SELECT i.*, c.name AS customer_name, c.address, c.phone, c.email, c.gst_no FROM invoices i JOIN customers c ON c.id=i.customer_id WHERE i.id='.(int)$id)->fetch_assoc();
    if(!$inv) json_response(['message'=>'Not found'],404);
    if (!is_admin_user()){
      $bid=(int)(sess_branch_id() ?? 0); if ((int)($inv['branch_id'] ?? 0) !== $bid) json_response(['message'=>'Forbidden'],403);
    }
    $items=[]; $res=$mysqli->query('SELECT it.*, fp.id AS fp_id, fp.final_height, fp.final_length, fp.final_sq_ft FROM invoice_items it LEFT JOIN final_products fp ON fp.id=it.final_product_id WHERE it.invoice_id='.(int)$id);
    while($r=$res->fetch_assoc()){ $items[]=$r; }
    $pays=[]; $sumPaid=0.0; $res2=$mysqli->query('SELECT * FROM payments WHERE invoice_id='.(int)$id.' ORDER BY id');
    while($r2=$res2->fetch_assoc()){ $pays[]=$r2; $sumPaid += (float)$r2['amount']; }
    $balance = max(0, (float)$inv['grand_total'] - $sumPaid);
    json_response(['data'=>['invoice'=>$inv,'items'=>$items,'payments'=>$pays,'total_paid'=>$sumPaid,'balance'=>$balance]]);
    break;

  case 'list':
    $q = trim($_GET['q'] ?? '');
    $status = trim($_GET['status'] ?? '');
    $branch_id = isset($_GET['branch_id']) ? (int)$_GET['branch_id'] : null;
    $customer_id = isset($_GET['customer_id']) ? (int)$_GET['customer_id'] : 0;
    $pay_term = trim($_GET['payment_term'] ?? '');
    $date = trim($_GET['date'] ?? ''); // daily filter (YYYY-MM-DD)
    $date_from = trim($_GET['date_from'] ?? '');
    $date_to = trim($_GET['date_to'] ?? '');
    $where=[]; $types=''; $params=[];
    if ($q !== '') { $where[]='(i.invoice_no LIKE CONCAT("%", ?, "%"))'; $types.='s'; $params[]=$q; }
    if ($status !== '' && in_array($status,['draft','issued','paid','cancelled'],true)) { $where[]='i.status=?'; $types.='s'; $params[]=$status; }
    if (!empty($customer_id)) { $where[]='i.customer_id=?'; $types.='i'; $params[]=$customer_id; }
    if ($pay_term !== '') { $where[]='i.payment_term=?'; $types.='s'; $params[]=$pay_term; }
    if ($date !== '') { $where[]='i.invoice_date=?'; $types.='s'; $params[]=$date; }
    else {
      if ($date_from !== '') { $where[]='i.invoice_date>=?'; $types.='s'; $params[]=$date_from; }
      if ($date_to !== '') { $where[]='i.invoice_date<=?'; $types.='s'; $params[]=$date_to; }
    }
    // Branch scope
    if (!is_admin_user()){
      $bid=(int)(sess_branch_id() ?? 0); $where[]='i.branch_id=?'; $types.='i'; $params[]=$bid;
    } else if (!empty($branch_id)) { $where[]='i.branch_id=?'; $types.='i'; $params[]=$branch_id; }
    $sql='SELECT i.id, i.invoice_no, i.invoice_date, i.status, i.payment_term, i.grand_total, c.name AS customer_name, i.branch_id,
                 IFNULL(pay.amt,0) AS total_paid,
                 GREATEST(i.grand_total - IFNULL(pay.amt,0), 0) AS balance
          FROM invoices i
          JOIN customers c ON c.id=i.customer_id
          LEFT JOIN (
            SELECT invoice_id, SUM(amount) AS amt
            FROM payments
            GROUP BY invoice_id
          ) pay ON pay.invoice_id=i.id';
    if ($where) $sql.=' WHERE '.implode(' AND ',$where);
    $sql.=' ORDER BY i.id DESC';
    if ($where){ $stmt=$mysqli->prepare($sql); $stmt->bind_param($types, ...$params); $stmt->execute(); $res=$stmt->get_result(); }
    else { $res=$mysqli->query($sql); }
    $rows=[]; while($r=$res->fetch_assoc()){ $rows[]=$r; }
    json_response(['data'=>$rows]);
    break;

  default:
    json_response(['message'=>'Invalid action'],400);
}
