<?php
/**
 * restaurant_cost_center_lib.php
 *
 * Shared cost-centre + supervisor-PIN library, ported verbatim from planner.php
 * so the POS and the planner use ONE identical system (same tables, statuses,
 * ledger, reports). Every function is guarded with function_exists so this file
 * can be safely included by both screens without collision.
 *
 * Depends on: restaurant_orders, restaurant_order_items, restaurant_payments,
 * restaurant_house_adjustments, restaurant_cost_centers, and money_fmt()/
 * r_current_user_id() which the including page already provides.
 */

if(!function_exists('money_fmt')){
    function money_fmt($v):string{ return number_format((float)$v,2); }
}
if(!function_exists('r_current_user_id')){
    function r_current_user_id(): ?int {
        foreach(['user_id','ss_user_id','id','uid'] as $k){
            if(isset($_SESSION[$k]) && (int)$_SESSION[$k]>0) return (int)$_SESSION[$k];
        }
        return null;
    }
}

if(!function_exists('r_table_exists')){
function r_table_exists(PDO $pdo,string $table):bool{ $s=$pdo->prepare("SELECT COUNT(*) FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name=:t");$s->execute([':t'=>$table]);return (int)$s->fetchColumn()>0; }
}

if(!function_exists('r_column_exists')){
function r_column_exists(PDO $pdo,string $table,string $column):bool{ $s=$pdo->prepare("SELECT COUNT(*) FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name=:t AND column_name=:c");$s->execute([':t'=>$table,':c'=>$column]);return (int)$s->fetchColumn()>0; }
}

if(!function_exists('r_value_matches_pin')){
function r_value_matches_pin(string $stored, string $pin): bool{
    $stored=trim($stored); $pin=trim($pin);
    if($stored==='' || $pin==='') return false;
    if(hash_equals($stored,$pin)) return true;
    if(strlen($stored)>=50 && function_exists('password_verify') && password_verify($pin,$stored)) return true;
    if(hash_equals(strtolower($stored),md5($pin))) return true;
    if(hash_equals(strtolower($stored),sha1($pin))) return true;
    return false;
}
}

if(!function_exists('r_role_is_supervisor')){
function r_role_is_supervisor(string $role): bool{
    $role=strtolower(trim($role));
    if($role==='') return false;
    foreach(['super_admin','superadmin','administrator','admin','master','owner','director','manager','supervisor'] as $ok){
        if($role===$ok || strpos($role,$ok)!==false) return true;
    }
    return false;
}
}

if(!function_exists('r_supervisor_pin_valid')){
function r_supervisor_pin_valid(PDO $pdo,string $pin): bool{
    $pin=trim($pin);
    if($pin==='') return false;
    $tables=['tblconsultant','consultants','users','system_users'];
    $pinCols=['supervisor_pin','manager_pin','admin_pin','void_pin','pin','auth_pin','password','Password','user_password','password_hash'];
    $roleCols=['role_key','user_role_key','ss_role_key','role','user_role','type','Role'];
    $activeCols=['is_active','active','status'];
    foreach($tables as $table){
        if(!r_table_exists($pdo,$table)) continue;
        $existingPinCols=[];
        foreach($pinCols as $pc){ if(r_column_exists($pdo,$table,$pc)) $existingPinCols[]=$pc; }
        if(!$existingPinCols) continue;
        $sql="SELECT * FROM `{$table}` WHERE 1=1";
        foreach($activeCols as $ac){
            if(r_column_exists($pdo,$table,$ac)){
                if($ac==='status') $sql.=" AND LOWER(CAST(`{$ac}` AS CHAR)) NOT IN ('0','inactive','disabled','deleted')";
                else $sql.=" AND COALESCE(`{$ac}`,1)=1";
                break;
            }
        }
        $sql.=" LIMIT 500";
        try{
            foreach($pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC) as $row){
                $roleOk=false;
                foreach($roleCols as $rc){
                    if(array_key_exists($rc,$row) && r_role_is_supervisor((string)$row[$rc])){ $roleOk=true; break; }
                }
                if(!$roleOk && isset($row['type']) && in_array((string)$row['type'],['1','master'],true)) $roleOk=true;
                if(!$roleOk) continue;
                foreach($existingPinCols as $pc){
                    if(array_key_exists($pc,$row) && r_value_matches_pin((string)$row[$pc],$pin)) return true;
                }
            }
        }catch(Throwable $ignore){}
    }
    return false;
}
}

if(!function_exists('r_mark_restaurant_item_split_allocation')){
function r_mark_restaurant_item_split_allocation(PDO $pdo,int $restaurantOrderId,int $itemId,int $splitId,int $unitNo,string $destinationType,int $folioId,string $payer,float $amount): void{
    if($restaurantOrderId<=0 || $itemId<=0) return;
    r_ensure_split_item_allocation_schema($pdo);
    $meta=r_split_destination_badge_label($pdo,$destinationType,$folioId,$payer);
    $roomNo=(string)($meta['room_no'] ?? '');
    $guestName=(string)($meta['guest_name'] ?? '');
    $payer=trim($payer);
    $dest=strtolower(trim($destinationType));

    $pdo->prepare("INSERT INTO restaurant_item_allocations
        (restaurant_order_id,restaurant_order_item_id,split_id,unit_no,destination_type,folio_id,room_no,guest_name,payer_name,amount,created_by)
        VALUES(:rid,:iid,:sid,:unit_no,:dest,:folio_id,:room_no,:guest_name,:payer,:amount,:created_by)")
        ->execute([
            ':rid'=>$restaurantOrderId,
            ':iid'=>$itemId,
            ':sid'=>$splitId>0?$splitId:null,
            ':unit_no'=>$unitNo>0?$unitNo:null,
            ':dest'=>$dest!==''?$dest:null,
            ':folio_id'=>$folioId>0?$folioId:null,
            ':room_no'=>$roomNo!==''?$roomNo:null,
            ':guest_name'=>$guestName!==''?$guestName:null,
            ':payer'=>$payer!==''?$payer:null,
            ':amount'=>round($amount,2),
            ':created_by'=>r_current_user_id()
        ]);

    if(r_table_exists($pdo,'restaurant_order_items')){
        try{
            $pdo->prepare("UPDATE restaurant_order_items
                SET split_target_type=:dest,
                    split_folio_id=:folio_id,
                    split_room_no=:room_no,
                    split_guest_name=:guest_name,
                    split_payer_name=:payer,
                    split_batch_id=:sid,
                    split_posted_at=NOW(),
                    split_posted_by=:created_by
                WHERE id=:iid AND restaurant_order_id=:rid")
                ->execute([
                    ':dest'=>$dest!==''?$dest:null,
                    ':folio_id'=>$folioId>0?$folioId:null,
                    ':room_no'=>$roomNo!==''?$roomNo:null,
                    ':guest_name'=>$guestName!==''?$guestName:null,
                    ':payer'=>$payer!==''?$payer:null,
                    ':sid'=>$splitId>0?$splitId:null,
                    ':created_by'=>r_current_user_id(),
                    ':iid'=>$itemId,
                    ':rid'=>$restaurantOrderId
                ]);
        }catch(Throwable $ignore){}
    }
}
}

if(!function_exists('r_ensure_cost_center_schema')){
function r_ensure_cost_center_schema(PDO $pdo): void{
    /*
       Internal costing departments (Staff Account, Management Account, Director
       Account, Marketing, etc.) let a meal be closed off without it being a real
       guest payment or a PMS room/master folio charge. It's an internal write-off,
       tracked in restaurant_house_adjustments for audit, same table already used
       by the split-bill "house_account"/"comp" destinations.
    */
    $pdo->exec("CREATE TABLE IF NOT EXISTS restaurant_cost_centers (
        id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
        property_id INT UNSIGNED NULL,
        name VARCHAR(120) NOT NULL,
        is_active TINYINT(1) NOT NULL DEFAULT 1,
        sort_order INT NOT NULL DEFAULT 1,
        created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
        INDEX idx_rest_cost_center_active(is_active,sort_order)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");

    try{
        $count=(int)$pdo->query("SELECT COUNT(*) FROM restaurant_cost_centers")->fetchColumn();
        if($count<=0){
            $pdo->exec("INSERT INTO restaurant_cost_centers (name,sort_order) VALUES
                ('Staff Account',1),
                ('Management Account',2),
                ('Director Account',3),
                ('Marketing Department',4)");
        }
    }catch(Throwable $ignore){}

    if(r_table_exists($pdo,'restaurant_orders')){
        try{
            $pdo->exec("ALTER TABLE restaurant_orders MODIFY payment_status ENUM('unpaid','part_paid','paid','room_charged','part_room_charged','cost_center_charged','part_cost_center_charged','void') NOT NULL DEFAULT 'unpaid'");
        }catch(Throwable $ignore){}
        if(!r_column_exists($pdo,'restaurant_orders','cost_center_id')){
            try{ $pdo->exec("ALTER TABLE restaurant_orders ADD COLUMN cost_center_id INT UNSIGNED NULL AFTER linked_folio_id"); }catch(Throwable $e){}
        }
    }

    if(!r_table_exists($pdo,'restaurant_house_adjustments')){
        r_ensure_simphony_style_schema($pdo);
    }
    if(r_table_exists($pdo,'restaurant_house_adjustments') && !r_column_exists($pdo,'restaurant_house_adjustments','cost_center_id')){
        try{ $pdo->exec("ALTER TABLE restaurant_house_adjustments ADD COLUMN cost_center_id INT UNSIGNED NULL AFTER adjustment_type"); }catch(Throwable $e){}
        try{ $pdo->exec("ALTER TABLE restaurant_house_adjustments ADD INDEX idx_rest_house_adj_cost_center(cost_center_id)"); }catch(Throwable $e){}
    }
}
}

if(!function_exists('r_cost_center_balances')){
function r_cost_center_balances(PDO $pdo): array{
    /*
       Running balance per costing department - the total ever posted against
       it (all-time). This is what makes "posting to account" actually behave
       like an account: charges accumulate and stay visible, rather than
       vanishing once the restaurant check itself closes.
    */
    r_ensure_cost_center_schema($pdo);
    $balances = [];
    if(!r_table_exists($pdo,'restaurant_house_adjustments') || !r_column_exists($pdo,'restaurant_house_adjustments','cost_center_id')){
        return $balances;
    }
    try{
        $stmt = $pdo->query("
            SELECT cost_center_id, COALESCE(SUM(amount),0) AS total_amount, COUNT(*) AS posting_count
            FROM restaurant_house_adjustments
            WHERE cost_center_id IS NOT NULL
            GROUP BY cost_center_id
        ");
        foreach($stmt->fetchAll(PDO::FETCH_ASSOC) as $row){
            $balances[(int)$row['cost_center_id']] = [
                'total' => (float)$row['total_amount'],
                'count' => (int)$row['posting_count'],
            ];
        }
    }catch(Throwable $ignore){}
    return $balances;
}
}

if(!function_exists('r_load_cost_centers')){
function r_load_cost_centers(PDO $pdo): array{
    r_ensure_cost_center_schema($pdo);
    try{
        return $pdo->query("SELECT * FROM restaurant_cost_centers WHERE is_active=1 ORDER BY sort_order ASC, name ASC")->fetchAll(PDO::FETCH_ASSOC) ?: [];
    }catch(Throwable $e){ return []; }
}
}

if(!function_exists('r_post_items_to_cost_center')){
function r_post_items_to_cost_center(PDO $pdo, int $restaurantOrderId, int $costCenterId, string $costCenterName, array $selectedItemIds, string $reason, string $pin, string $note='', string $discountType='none', float $discountValue=0.0): void{
    r_ensure_cost_center_schema($pdo);

    if($restaurantOrderId<=0) throw new RuntimeException('Invalid restaurant order.');
    $reason=trim($reason);
    if($reason==='') throw new RuntimeException('Please enter a reason for this internal charge.');
    if(!r_supervisor_pin_valid($pdo,$pin)) throw new RuntimeException('Invalid supervisor PIN or insufficient authority.');

    $discountType=strtolower(trim($discountType));
    if(!in_array($discountType,['none','amount','percent'],true)) $discountType='none';
    if($discountType!=='none' && $discountValue<0) throw new RuntimeException('Discount value cannot be negative.');
    if($discountType==='percent' && $discountValue>100) throw new RuntimeException('Discount percentage cannot exceed 100%.');

    r_recalc($pdo,$restaurantOrderId);

    $ro=$pdo->prepare("SELECT * FROM restaurant_orders WHERE id=:id LIMIT 1");
    $ro->execute([':id'=>$restaurantOrderId]);
    $ord=$ro->fetch(PDO::FETCH_ASSOC);
    if(!$ord) throw new RuntimeException('Restaurant order not found.');

    $postingSelectedItems = !empty($selectedItemIds);
    $baseAmount = round((float)($ord['total_amount'] ?? 0), 2);
    $postedItemNames = [];

    if($postingSelectedItems){
        $in=implode(',',array_fill(0,count($selectedItemIds),'?'));
        $params=array_merge([$restaurantOrderId],$selectedItemIds);
        $stItems=$pdo->prepare("SELECT id,item_name,qty,line_total FROM restaurant_order_items WHERE restaurant_order_id=? AND id IN ($in) AND status='active' ORDER BY id ASC");
        $stItems->execute($params);
        $selectedRows=$stItems->fetchAll(PDO::FETCH_ASSOC) ?: [];
        if(!$selectedRows) throw new RuntimeException('Select at least one active item to post, or untick all items to post the full bill.');

        $baseAmount=0.00;
        foreach($selectedRows as $sr){
            $lineAmount=round((float)($sr['line_total'] ?? 0),2);
            $baseAmount += $lineAmount;
            $postedItemNames[] = rtrim(rtrim(number_format((float)($sr['qty'] ?? 1),2,'.',''),'0'),'.').' x '.trim((string)($sr['item_name'] ?? 'Item'));
            r_mark_restaurant_item_split_allocation($pdo,$restaurantOrderId,(int)$sr['id'],0,0,'cost_center',0,$costCenterName,$lineAmount);
        }
        $baseAmount=round($baseAmount,2);
    }

    if($baseAmount<=0) throw new RuntimeException('There is no amount to post to this costing department.');

    $discountAmount = 0.00;
    if($discountType==='amount'){
        $discountAmount = min($baseAmount, round($discountValue,2));
    }elseif($discountType==='percent'){
        $discountAmount = round($baseAmount * ($discountValue/100), 2);
    }
    $amount = round(max(0, $baseAmount - $discountAmount), 2);
    if($amount<=0) throw new RuntimeException('The discount leaves nothing to post. Check the discount value.');

    $adjReason = $costCenterName.': '.$reason.($note!==''?' - '.$note:'');
    if($discountAmount>0){
        $discountLabel = $discountType==='percent' ? ($discountValue.'%') : money_fmt($discountValue);
        $adjReason .= ' | Discount '.$discountLabel.' ('.money_fmt($discountAmount).' off '.money_fmt($baseAmount).')';
    }
    if($postingSelectedItems){
        $adjReason .= ' | Items: '.implode('; ',array_slice($postedItemNames,0,12));
        if(count($postedItemNames)>12) $adjReason .= '; +'.(count($postedItemNames)-12).' more';
    }

    $pdo->prepare("INSERT INTO restaurant_house_adjustments(restaurant_order_id,split_id,adjustment_type,cost_center_id,amount,reason,created_by) VALUES(:rid,NULL,'house_account',:cc,:amount,:reason,:u)")->execute([
        ':rid'=>$restaurantOrderId,
        ':cc'=>$costCenterId,
        ':amount'=>$amount,
        ':reason'=>$adjReason,
        ':u'=>r_current_user_id()
    ]);

    /*
       Internal Account postings always close the table, whether the full
       bill or only selected items were posted. Unlike Room Folio (which
       supports genuinely leaving a check open for further guest billing),
       once a check is written off to an internal department it is treated
       as settled and the table is released.
    */
    $noteAdd='Posted to costing department '.$costCenterName.' on '.date('Y-m-d H:i:s').' | Amount '.money_fmt($amount)
        .($discountAmount>0 ? ' (after '.money_fmt($discountAmount).' discount off '.money_fmt($baseAmount).')' : '')
        .' | Reason: '.$reason.($note!==''?' - '.$note:'')
        .($postingSelectedItems ? (' | '.implode('; ',array_slice($postedItemNames,0,12))) : ' | Full bill');

    $pdo->prepare("UPDATE restaurant_orders SET cost_center_id=:cc, status='closed', payment_status='cost_center_charged', close_method=:cm, closed_by=:u, closed_at=NOW(), notes=TRIM(CONCAT(COALESCE(notes,''), :note_add)) WHERE id=:rid")->execute([
        ':cc'=>$costCenterId,
        ':cm'=>'Posted to '.$costCenterName.($discountAmount>0?' (discounted)':''),
        ':u'=>r_current_user_id(),
        ':note_add'=>"\n".$noteAdd,
        ':rid'=>$restaurantOrderId
    ]);
    r_recipe_stock_flash(r_post_recipe_stock_sale_movements($pdo,$restaurantOrderId));
    r_release_restaurant_tables($pdo,$restaurantOrderId,!empty($ord['table_id'])?(int)$ord['table_id']:null);
}
}


if(!function_exists('r_format')){
    function r_format($v): string { return number_format((float)$v, 2); }
}
if(!function_exists('r_recalc')){
    function r_recalc(PDO $pdo, int $rid): void {
        if (function_exists('fo_recalc')) { fo_recalc($pdo, $rid); return; }
        $s = $pdo->prepare("SELECT COALESCE(SUM(line_total),0) sub, COALESCE(SUM(tax_amount),0) tax FROM restaurant_order_items WHERE restaurant_order_id=:id AND status='active'");
        $s->execute([':id'=>$rid]); $r = $s->fetch(PDO::FETCH_ASSOC) ?: [];
        $sub=(float)($r['sub']??0); $tax=(float)($r['tax']??0);
        $pdo->prepare("UPDATE restaurant_orders SET subtotal=:s, tax_total=:t, total_amount=:g, updated_at=NOW() WHERE id=:id")->execute([':s'=>round($sub,2),':t'=>round($tax,2),':g'=>round($sub,2),':id'=>$rid]);
    }
}
if(!function_exists('r_post_recipe_stock_sale_movements')){
    function r_post_recipe_stock_sale_movements(PDO $pdo, int $restaurantOrderId): array { return []; }
}
if(!function_exists('r_recipe_stock_flash')){
    function r_recipe_stock_flash(array $result): void { }
}
if(!function_exists('r_release_restaurant_tables')){
    function r_release_restaurant_tables(PDO $pdo, int $restaurantOrderId, ?int $fallbackTableId=null): void { }
}


/* Additional planner helpers required by the cost-centre poster, ported verbatim. */
if(!function_exists('r_ensure_split_item_allocation_schema')){
function r_ensure_split_item_allocation_schema(PDO $pdo): void{
    if(r_table_exists($pdo,'restaurant_order_items')){
        $cols=[
            'split_target_type' => "VARCHAR(40) NULL",
            'split_folio_id' => "INT UNSIGNED NULL",
            'split_room_no' => "VARCHAR(80) NULL",
            'split_guest_name' => "VARCHAR(180) NULL",
            'split_payer_name' => "VARCHAR(180) NULL",
            'split_batch_id' => "BIGINT UNSIGNED NULL",
            'split_posted_at' => "DATETIME NULL",
            'split_posted_by' => "INT UNSIGNED NULL"
        ];
        foreach($cols as $col=>$def){
            if(!r_column_exists($pdo,'restaurant_order_items',$col)){
                try{ $pdo->exec("ALTER TABLE restaurant_order_items ADD COLUMN {$col} {$def}"); }catch(Throwable $ignore){}
            }
        }
    }

    $pdo->exec("CREATE TABLE IF NOT EXISTS restaurant_item_allocations (
        id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
        restaurant_order_id BIGINT UNSIGNED NOT NULL,
        restaurant_order_item_id BIGINT UNSIGNED NOT NULL,
        split_id BIGINT UNSIGNED NULL,
        unit_no INT UNSIGNED NULL,
        destination_type VARCHAR(40) NULL,
        folio_id INT UNSIGNED NULL,
        room_no VARCHAR(80) NULL,
        guest_name VARCHAR(180) NULL,
        payer_name VARCHAR(180) NULL,
        amount DECIMAL(14,2) NOT NULL DEFAULT 0.00,
        created_by INT UNSIGNED NULL,
        created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
        INDEX idx_rest_item_alloc_order(restaurant_order_id),
        INDEX idx_rest_item_alloc_item(restaurant_order_item_id),
        INDEX idx_rest_item_alloc_folio(folio_id),
        INDEX idx_rest_item_alloc_split(split_id)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");

    /*
       Defensive fix: if this table already existed (e.g. from before cost-center
       postings, which have no folio at all) with folio_id/split_id/room_no/etc.
       defined as NOT NULL, the CREATE TABLE IF NOT EXISTS above is a no-op and
       never relaxes those columns. Force them nullable explicitly so a cost
       center posting (folio_id = NULL) doesn't fail with
       "Column 'folio_id' cannot be null".
    */
    foreach(['folio_id'=>'INT UNSIGNED NULL','split_id'=>'BIGINT UNSIGNED NULL','unit_no'=>'INT UNSIGNED NULL','room_no'=>'VARCHAR(80) NULL','guest_name'=>'VARCHAR(180) NULL','payer_name'=>'VARCHAR(180) NULL','destination_type'=>'VARCHAR(40) NULL'] as $col=>$def){
        try{ $pdo->exec("ALTER TABLE restaurant_item_allocations MODIFY {$col} {$def}"); }catch(Throwable $ignore){}
    }
}
}

if(!function_exists('r_split_destination_badge_label')){
function r_split_destination_badge_label(PDO $pdo, string $destinationType, int $folioId, string $payer=''): array{
    $dest=strtolower(trim($destinationType));
    $payer=trim($payer);
    $folio=$folioId>0 ? r_folio_print_label($pdo,$folioId) : ['room_no'=>'','folio_name'=>'','label'=>''];
    $roomNo=trim((string)($folio['room_no'] ?? ''));
    $guestName=trim((string)($folio['folio_name'] ?? ''));
    $label='';

    if($roomNo!==''){
        $label='Room '.$roomNo;
    }elseif($guestName!==''){
        $label=$guestName;
    }elseif($payer!==''){
        $label=$payer;
    }else{
        $label=ucwords(str_replace('_',' ',$dest ?: 'Split'));
    }

    if(strpos($dest,'master')!==false && $roomNo==='') $label='Master Folio';
    if(strpos($dest,'guest')!==false && $roomNo==='' && $guestName!=='') $label='Guest: '.$guestName;

    return ['room_no'=>$roomNo,'guest_name'=>$guestName,'label'=>$label];
}
}


if(!function_exists('r_ensure_simphony_style_schema')){
    function r_ensure_simphony_style_schema(PDO $pdo): void { /* POS: base restaurant tables already exist. */ }
}
if(!function_exists('r_folio_print_label')){
    function r_folio_print_label(PDO $pdo, int $folioId): array {
        $out = ['room_no'=>'','folio_name'=>'','label'=>''];
        try {
            $s = $pdo->prepare("SELECT folio_no, folio_name FROM folios WHERE id=:id LIMIT 1");
            $s->execute([':id'=>$folioId]);
            $f = $s->fetch(PDO::FETCH_ASSOC) ?: [];
            $out['folio_name'] = (string)($f['folio_name'] ?? '');
            $out['label'] = trim((string)($f['folio_no'] ?? '') . ' ' . (string)($f['folio_name'] ?? ''));
        } catch (Throwable $e) {}
        return $out;
    }
}
