<?php
declare(strict_types=1);

/**
 * config/functions.php
 * Common helpers used across CMRS.
 *
 * NOTE:
 * - Keep all connection logic here (db()).
 * - Ensure config/config.php DOES NOT declare db() / project_root() again.
 */

//// Bootstrap ////

// Load app config & constants (safe to include; they must not define functions)
require_once __DIR__ . '/config.php';
if (file_exists(__DIR__ . '/constants.php')) {
  require_once __DIR__ . '/constants.php';
}

if (!defined('APP_ENV'))  define('APP_ENV', 'prod');
if (!defined('APP_NAME')) define('APP_NAME', 'Chauffeur Management & Rental System');

// Default vehicle types if not defined in constants.php
if (!defined('VEHICLE_TYPES')) {
  define('VEHICLE_TYPES', [
    'Saloon Car','Business Sedan','First Class Sedan',
    'Business Van','MPV5','MPV8','Mini Bus','Coach'
  ]);
}

//// Session ////

function ensure_session(): void {
  if (session_status() !== PHP_SESSION_ACTIVE) {
    // More secure defaults
    session_set_cookie_params([
      'lifetime' => 0,
      'httponly' => true,
      'secure'   => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on',
      'samesite' => 'Lax',
      'path'     => '/',
    ]);
    session_start();
  }
}

ensure_session();

//// DB ////

/**
 * Singleton PDO connection (UTF8MB4, exceptions, assoc fetch).
 */
function db(): PDO {
  static $pdo = null;
  if ($pdo instanceof PDO) return $pdo;

  // Expect these to be defined by config/config.php
  $dsn  = defined('DB_DSN')  ? DB_DSN  : '';
  $user = defined('DB_USER') ? DB_USER : '';
  $pass = defined('DB_PASS') ? DB_PASS : '';

  if ($dsn === '') {
    throw new RuntimeException('DB_DSN not configured. Check config/config.php');
  }

  $pdo = new PDO($dsn, $user, $pass, [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES   => false,
  ]);
  $pdo->exec("SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci");
  return $pdo;
}

//// Paths & URLs ////

/** Filesystem project root (…/cmrs) */
function project_root(): string {
  // config/ is one level below project root
  return dirname(__DIR__);
}

/**
 * Compute absolute root URL like https://host/<cmrs>/
 * Works reliably whether current script is under /public/* or /modules/*.
 * If BASE_URL is defined (e.g. in config.php), that takes precedence.
 */
function root_url(): string {
  if (defined('BASE_URL') && BASE_URL) {
    return rtrim(BASE_URL, '/') . '/';
  }

  $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') ? 'https' : 'http';
  $host   = $_SERVER['HTTP_HOST'] ?? 'localhost';
  $script = $_SERVER['SCRIPT_NAME'] ?? '/';

  // Try to detect "<root>/public/" or "<root>/modules/" and strip from there
  $rootPath = '/';
  if (preg_match('#^(.*?)/(public|modules)/#', $script, $m)) {
    $rootPath = $m[1] ?: '/';
  } else {
    // Fallback: go up one directory from script
    $rootPath = rtrim(dirname($script), '/\\');
    if ($rootPath === '' || $rootPath === '\\') $rootPath = '/';
  }

  return rtrim($scheme . '://' . $host . $rootPath, '/') . '/';
}

/** URL helpers */
function url_public(string $path = ''): string {
  return root_url() . 'public/' . ltrim($path, '/');
}
function url_modules(string $path = ''): string {
  return root_url() . 'modules/' . ltrim($path, '/');
}
function url_asset(string $path = ''): string {
  return root_url() . 'public/assets/' . ltrim($path, '/');
}

//// Output ////

/** Escape for HTML */
function e(mixed $v): string {
  return htmlspecialchars((string)$v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

//// Auth ////

function current_user(): array {
  return $_SESSION['user'] ?? [];
}

function is_logged_in(): bool {
  return !empty($_SESSION['user']['id']);
}

function require_login(): void {
  if (!is_logged_in()) {
    redirect(url_public('login.php'));
  }
}

/** Normalize common role aliases to canonical short names used in code. */
function normalize_role_name(?string $name): string {
  $r = strtolower(trim((string)$name));
  $map = [
    'system administrator' => 'admin',
    'administrator'        => 'admin',
    'system admin'         => 'admin',
    'sysadmin'             => 'admin',
    'admin'                => 'admin',

    'managing director'    => 'md',
    'md'                   => 'md',

    'accounts'             => 'accounts',
    'accountant'           => 'accounts',
    'finance'              => 'accounts',

    'ops'                  => 'ops',
    'operations'           => 'ops',

    'management'           => 'management',
    'manager'              => 'management',
    
        // ✨ Sales aliases (NEW)
    'sales'                => 'sales',
    'sales manager'        => 'sales',
    'salesmanager'         => 'sales',
    'sales executive'      => 'sales',
    'sales team'           => 'sales',
    'business development' => 'sales',
    'bdm'                  => 'sales',
  ];
  return $map[$r] ?? $r;
}

/**
 * Role-based access: accepts a single role or array of roles (names).
 * Example: require_role(['Admin','Ops']);
 * Now tolerant to aliases like "System Administrator" -> "Admin".
 */
function require_role(string|array $roles): void {
  $roles = (array)$roles;
  $user  = current_user();
  $raw   = $user['role'] ?? $user['role_name'] ?? null;

  // Canonicalize both sides (case/alias insensitive)
  $userRole = normalize_role_name($raw);
  $allowed  = array_map(fn($r) => normalize_role_name((string)$r), $roles);

  if (!$userRole || !in_array($userRole, array_map('strtolower', $allowed), true)) {
    // Optional: 403 page; for now, send to dashboard
    redirect(url_public('index.php'));
  }
}

/**
 * Helper requested: returns the user's role name from session.
 * Used by includes/header.php.
 */
function user_role_name(): string {
  $u = current_user();
  return (string)($u['role'] ?? $u['role_name'] ?? '');
}


// Detect client IP (safe fallback if behind proxies)
function client_ip(): string {
    $keys = [
        'HTTP_CLIENT_IP',
        'HTTP_X_FORWARDED_FOR',
        'HTTP_X_FORWARDED',
        'HTTP_X_CLUSTER_CLIENT_IP',
        'HTTP_FORWARDED_FOR',
        'HTTP_FORWARDED',
        'REMOTE_ADDR'
    ];
    foreach ($keys as $key) {
        if (!empty($_SERVER[$key])) {
            $ipList = explode(',', $_SERVER[$key]);
            return trim($ipList[0]);
        }
    }
    return '0.0.0.0';
}

// Detect client User Agent
function client_ua(): string {
    return $_SERVER['HTTP_USER_AGENT'] ?? 'unknown';
}

//// CSRF ////

function csrf_token(): string {
  if (empty($_SESSION['csrf'])) {
    $_SESSION['csrf'] = bin2hex(random_bytes(32));
  }
  return $_SESSION['csrf'];
}

function csrf_verify(string $token): void {
  if (!hash_equals($_SESSION['csrf'] ?? '', $token)) {
    throw new RuntimeException('Invalid CSRF token');
  }
}

//// HTTP ////

function redirect(string $url): void {
  header('Location: ' . $url);
  exit;
}

//// Metrics ////

/**
 * Safe table counter for known tables.
 */
function table_count(string $table): ?int {
  static $allowed = ['bookings','drivers','vehicles','rentals','invoices','partners'];
  if (!in_array($table, $allowed, true)) return null;
  $stmt = db()->query("SELECT COUNT(*) AS c FROM `$table`");
  $row  = $stmt->fetch();
  return (int)($row['c'] ?? 0);
}

//// Audit ////

/**
 * Audit logger.
 * @param string $action  e.g. 'login.success', 'booking.create'
 * @param string $entity  e.g. 'booking', 'driver'
 * @param int    $entityId
 * @param mixed  $diff    extra info (array/object will be json-encoded)
 */
function audit_log(string $action, string $entity, int $entityId = 0, mixed $diff = null): void {
  try {
    $user = current_user();
    $uid  = (int)($user['id'] ?? 0);
    $json = $diff === null ? null : json_encode($diff, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    $stmt = db()->prepare("
      INSERT INTO audit_logs (user_id, action, entity_type, entity_id, diff, timestamp)
      VALUES (:uid, :action, :etype, :eid, :diff, NOW())
    ");
    $stmt->execute([
      ':uid'   => $uid,
      ':action'=> $action,
      ':etype' => $entity,
      ':eid'   => $entityId,
      ':diff'  => $json,
    ]);
  } catch (Throwable $e) {
    if (APP_ENV === 'dev') {
      error_log('audit_log error: ' . $e->getMessage());
    }
  }
}

//// Booking Helpers ////

/**
 * Generate a booking reference with a random 6-digit suffix.
 * Format: BHC-DDMMYYYY-XXXXXX
 * - XXXXXX is a random int [100000..999999]
 * - Ensures uniqueness per (company_id, booking_ref)
 *
 * @param int         $companyId
 * @param string|null $pickupDate        'Y-m-d' (uses today if null/invalid)
 * @param int|null    $excludeBookingId  pass current booking id when editing (optional)
 * @return string
 */
function generate_booking_ref(int $companyId, ?string $pickupDate = null, ?int $excludeBookingId = null): string
{
    // Normalize date
    $date = (is_string($pickupDate) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $pickupDate))
        ? $pickupDate
        : date('Y-m-d');

    $ddmmyyyy = date('dmY', strtotime($date));
    $prefix   = "BHC-{$ddmmyyyy}-";

    // Prepare uniqueness check
    $sql = "SELECT 1 FROM bookings WHERE company_id = :cid AND booking_ref = :ref";
    if ($excludeBookingId) $sql .= " AND id <> :bid";
    $sql .= " LIMIT 1";
    $check = db()->prepare($sql);

    // Try a few random candidates before falling back
    for ($attempt = 0; $attempt < 10; $attempt++) {
        $suffix = (string)random_int(100000, 999999);
        $ref    = $prefix . $suffix;

        $params = [':cid' => $companyId, ':ref' => $ref];
        if ($excludeBookingId) $params[':bid'] = $excludeBookingId;

        $check->execute($params);
        if (!$check->fetchColumn()) {
            return $ref; // unique -> done
        }
    }

    // Extreme fallback: timestamp + 2 random digits (still readable)
    $ref = $prefix . date('His') . random_int(10, 99);
    return $ref;
}


/**
 * Compute profit for a booking row (array fields from SELECT).
 * Profit = (client + client extras) - (driver + driver extras) - commission
 */
function booking_profit(array $r): float {
  $client  = (float)($r['total_client_price'] ?? 0);
  $driver  = (float)($r['total_driver_price'] ?? 0);
  $cExtras = (float)($r['client_parking_fee'] ?? 0) + (float)($r['client_waiting_fee'] ?? 0);
  $dExtras = (float)($r['driver_parking_fee'] ?? 0) + (float)($r['driver_waiting_fee'] ?? 0);
  $commission = (float)($r['partner_commission_amount'] ?? 0);
  return ($client + $cExtras) - ($driver + $dExtras) - $commission;
}

//// Small Utilities ////

/** Return POST value (trimmed) with default */
function post_str(string $key, ?string $default = null): ?string {
  return isset($_POST[$key]) ? trim((string)$_POST[$key]) : $default;
}

/** Return GET value (trimmed) with default */
function get_str(string $key, ?string $default = null): ?string {
  return isset($_GET[$key]) ? trim((string)$_GET[$key]) : $default;
}

/** Parse decimal amount safely */
function to_dec(string|float|int|null $v): float {
  if ($v === null || $v === '') return 0.0;
  return (float)preg_replace('/[^0-9.\-]/', '', (string)$v);
}

/** Build a human friendly date/time string */
function dt_label(?string $date, ?string $time): string {
  $date = $date ?: '';
  $time = $time ?: '';
  return trim($date . ' ' . $time);
}
