<?php

/**
 * ============================================================
 *  Booking Availability Middleware
 *  Interakt WhatsApp Template  <-->  Magento 1.9 REST API
 * ============================================================
 *  Endpoints
 *  ---------
 *  GET  ?exec=claim_sig   -> Available
 *  GET  ?exec=claim_sig   -> Not Available
 *  URL: ?exec=claim_sig  - booking ID is inside $claim, not readable without decoding
 * 
 *  Security layers:
 *  1. HMAC-SHA256 signature validation
 *  2. Token expiry check
 *  3. Per-booking-ID one-time use (date-keyed JSON file) Once ANY action fires for a booking ID, ALL links for that booking ID are permanently blocked.
 *
 *  PHP 8.1.2
 * Satyam on 25-05-2026
 * ============================================================
 */

declare(strict_types=1);

// -----------------------
// 0. CONFIGURATION
// -----------------------
define('HMAC_SECRET',          '7703844eb6ec6d2ce7e92352e3e8b4e3');
define('TOKEN_TTL_SECONDS',    28800);

// Magento 1.9 REST
define('MAGENTO_API_URL',     'https://yatradham.org/restapiv2.php?apiname=orderoperation');
define('MAGENTO_API_TOKEN',    'a480459a8f467d6e211b8b5c');
define('COMPANY_LOGO_URL', 'https://cdn.yatradham.org/skin/frontend/default/ydhome/yd_newtheme/images/logo.png');
define('COMPANY_NAME',     'YatraDham.Org');

// Attempt log directory (writable by web server, outside webroot ideally)
define('ATTEMPT_LOG_DIR',      __DIR__ . '/logs/attempts');

// -----------------------
// 1. TOKEN VALIDATION
// -----------------------

/**
 * Parse and validate the raw ?exec= string.
 */
function validateExecParam(string $exec): array
{
    $parts = explode('_', $exec, 2);  // [claim, sig]
    if (count($parts) !== 2) {
        return ['valid' => false, 'error' => 'Malformed request.'];
    }

    [$claim, $sig] = $parts;

    // Verify signature first - before decoding anything
    $expectedSig = hash_hmac('sha256', $claim, HMAC_SECRET);
    if (!hash_equals($expectedSig, $sig)) {
        return ['valid' => false, 'error' => 'Invalid or tampered link.'];
    }

    // Only decode after signature passes
    $decoded = base64_decode($claim, strict: true);
    if ($decoded === false) {
        return ['valid' => false, 'error' => 'Malformed token.'];
    }

    // Expected: action|bookingId|expires
    $innerParts = explode('|', $decoded, 4);
    if (count($innerParts) !== 4) {
        return ['valid' => false, 'error' => 'Malformed token payload.'];
    }

    [$action, $bookingId, $expires, $managerMobile] = $innerParts;

    if (!in_array($action, ['av1', 'av0'], true)) {
        return ['valid' => false, 'error' => 'Unknown action.'];
    }

    if (!ctype_digit($expires) || !ctype_digit($bookingId)) {
        return ['valid' => false, 'error' => 'Invalid parameter format.' . json_encode($innerParts)];
    }

    if (time() > (int) $expires) {
        return ['valid' => false, 'error' => 'This link has expired.'];
    }

    return [
        'valid' => true,
        'action' => $action,
        'booking_id' => $bookingId,
        'manager_mobile' => $managerMobile,
    ];
}

// -----------------------
// 2. ONE-TIME USE - per booking ID, date-keyed JSON
// -----------------------

/**
 * Returns path to today's attempt log file.
 * Format: logs/attempts/2026-05-26.json
 *
 * We keep date-keyed files so old logs can be archived/deleted
 * without touching active ones. We also search yesterday's file
 * for tokens that were generated near midnight.
 */
function getAttemptFilePath(string $date): string
{
    return ATTEMPT_LOG_DIR . '/' . $date . '.json';
}

/**
 * Check whether this booking ID has already been actioned.
 * Searches today's file AND yesterday's file (cross-midnight safety).
 */
function isBookingAlreadyActioned(string $bookingId): bool
{
    $dates = [
        date('Y-m-d'),
        date('Y-m-d', strtotime('-1 day')),
    ];

    foreach ($dates as $date) {
        $file = getAttemptFilePath($date);
        if (!file_exists($file)) {
            continue;
        }

        $data = json_decode((string) file_get_contents($file), true);
        if (is_array($data) && array_key_exists($bookingId, $data)) {
            return true;
        }
    }

    return false;
}

/**
 * Mark a booking ID as actioned in today's log file.
 * Uses an exclusive file lock to prevent race conditions
 * (two managers clicking simultaneously).
 *
 * Stored record:
 * {
 *   "1023": {
 *     "action":     "av1",
 *     "actioned_at": "2026-05-26 14:32:11",
 *     "ip":          "103.x.x.x"
 *   }
 * }
 */
function markBookingActioned(string $bookingId, string $action, string $managerMobile): bool
{
    if (!is_dir(ATTEMPT_LOG_DIR)) {
        mkdir(ATTEMPT_LOG_DIR, 0755, true);
    }

    $file = getAttemptFilePath(date('Y-m-d'));
    $fp   = fopen($file, 'c+');  // open for r/w, create if missing, no truncate

    if (!$fp) {
        return false;
    }

    if (!flock($fp, LOCK_EX)) {
        fclose($fp);
        return false;
    }

    // Read current contents
    $size    = filesize($file) ?: 0;
    $content = $size > 0 ? fread($fp, $size) : '';
    $data    = json_decode((string) $content, true);

    if (!is_array($data)) {
        $data = [];
    }

    // Double-check inside lock - prevent race condition
    if (array_key_exists($bookingId, $data)) {
        flock($fp, LOCK_UN);
        fclose($fp);
        return false; // already actioned by a concurrent request
    }

    // Record the action
    $data[$bookingId] = [
        'action'      => $action,
        'manager_mobile'      => $managerMobile,
        'actioned_at' => date('Y-m-d H:i:s'),
        'ip'          => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
    ];

    // Write back
    ftruncate($fp, 0);
    rewind($fp);
    fwrite($fp, json_encode($data, JSON_PRETTY_PRINT));
    fflush($fp);
    flock($fp, LOCK_UN);
    fclose($fp);

    return true;
}

// -----------------------
// 3. MAGENTO 1.9 REST API
// -----------------------

/**
 * PUT booking availability status to Magento.
 * to match your custom Magento module's REST route.
 */
function updateMagentoBooking(string $bookingId, string $status, string $managerMobile): array
{
    $payload = http_build_query(array(
        'booking_id'          => $bookingId,
        'availability_status' => $status,
        'mobile_number' => $managerMobile
    ));

    $ch = curl_init();
    curl_setopt_array($ch, array(
        CURLOPT_URL            => MAGENTO_API_URL,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => $payload,
        CURLOPT_CONNECTTIMEOUT => 3,
        CURLOPT_TIMEOUT        => 8,
        CURLOPT_HTTPHEADER     => array(
            'Accept: application/json',
            'Content-Type: application/x-www-form-urlencoded',
            'key: ' . MAGENTO_API_TOKEN,
        ),
    ));

    $response  = curl_exec($ch);
    $httpCode  = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $curlError = curl_error($ch);
    curl_close($ch);

    if ($curlError) {
        return ['success' => false, 'error' => "cURL error: {$curlError}"];
    }
    $decoded = json_decode((string) $response, true);

    if ($httpCode >= 200 && $httpCode < 300) {
        return [
            'success' => empty($decoded['error']),
            'data'    => $decoded
        ];
    }

    return [
        'success' => false,
        'error'   => "Magento HTTP {$httpCode}: " . ($decoded['message'] ?? (string) $response),
    ];
}


// -----------------------
// 4. LOGGING
// -----------------------

function logEvent(string $level, string $message, array $context = []): void
{
    $logDir = __DIR__ . '/logs';
    if (!is_dir($logDir)) {
        mkdir($logDir, 0750, true);
    }

    $line = sprintf(
        "[%s] [%s] %s %s\n",
        date('Y-m-d H:i:s'),
        strtoupper($level),
        $message,
        $context ? json_encode($context) : ''
    );

    file_put_contents($logDir . '/middleware.log', $line, FILE_APPEND | LOCK_EX);
}

// -----------------------
// 5. CONFIRMATION PAGE
// -----------------------

function renderPage(bool $success, string $heading, string $body): never
{
    // ── Pre-compute all dynamic values — no expressions in heredoc ──
    $borderColor = $success ? '#15803d' : '#b91c1c';
    $iconBg      = $success ? '#f0fdf4' : '#fef2f2';
    $iconColor   = $success ? '#16a34a' : '#dc2626';
    $iconChar    = $success ? '&#10003;' : '&#10007;';
    $safeHeading = htmlspecialchars($heading, ENT_QUOTES, 'UTF-8');
    $safeBody    = htmlspecialchars($body,    ENT_QUOTES, 'UTF-8');

    // ── Branding — update these two constants at top of your config ──
    $logoUrl     = COMPANY_LOGO_URL;   // e.g. 'https://yourstore.com/logo.png'
    $companyName = COMPANY_NAME;       // e.g. 'Your Property Name'
    $safeCompany = htmlspecialchars($companyName, ENT_QUOTES, 'UTF-8');

    http_response_code($success ? 200 : 400);
    header('Content-Type: text/html; charset=UTF-8');

    echo <<<HTML
        <!DOCTYPE html>
        <html lang="en">
        <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Booking Response — {$safeCompany}</title>
        <style>
            *{box-sizing:border-box;margin:0;padding:0}
            body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
                background:#f4f4f5;display:flex;flex-direction:column;align-items:center;
                justify-content:center;min-height:100vh;padding:20px;gap:16px}
            .card{background:#fff;border-top:4px solid {$borderColor};
                border-radius:10px;width:100%;max-width:400px;
                box-shadow:0 1px 6px rgba(0,0,0,.07);overflow:hidden}
            .card-header{padding:24px 32px 20px;text-align:center;
                        border-bottom:1px solid #f0f0f0}
            .logo{max-height:48px;max-width:160px;width:auto;
                object-fit:contain;display:block;margin:0 auto}
            .card-body{padding:28px 32px;text-align:center}
            .icon{width:52px;height:52px;border-radius:50%;background:{$iconBg};
                color:{$iconColor};font-size:24px;line-height:52px;
                margin:0 auto 16px;font-weight:700}
            h1{font-size:18px;font-weight:600;color:#111;margin-bottom:8px}
            p{font-size:14px;color:#555;line-height:1.6}
            .note{margin-top:16px;font-size:12px;color:#bbb}
            .card-footer{padding:14px 32px;text-align:center;
                        background:#fafafa;border-top:1px solid #f0f0f0}
            .footer-text{font-size:11px;color:#aaa;letter-spacing:.01em}
            .footer-text strong{color:#888;font-weight:500}
        </style>
        </head>
        <body>
        <div class="card">

            <!-- Header with logo -->
            <div class="card-header">
            <img src="{$logoUrl}"
                alt="{$safeCompany}"
                class="logo"
                onerror="this.style.display='none'">
            </div>

            <!-- Status body -->
            <div class="card-body">
            <div class="icon">{$iconChar}</div>
            <h1>{$safeHeading}</h1>
            <p>{$safeBody}</p>
            <p class="note">You can close this window.</p>
            </div>

            <!-- Footer with company name -->
            <div class="card-footer">
            <p class="footer-text">
                &copy; {$safeCompany} &nbsp;|&nbsp; Secure Booking System
            </p>
            </div>

        </div>
        </body>
        </html>
        HTML;
    exit;
}

// -----------------------
// 6. MAIN - only GET requests accepted
// -----------------------

if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'GET' || !isset($_GET['exec'])) {
    http_response_code(400);
    exit;
}

$exec = trim((string) ($_GET['exec'] ?? ''));

// -- Step 1: Validate token --------------------
$validation = validateExecParam($exec);

if (!$validation['valid']) {
    logEvent('warn', 'Token validation failed', [
        'error' => $validation['error'],
        'ip'    => $_SERVER['REMOTE_ADDR'] ?? '',
    ]);
    renderPage(false, 'Invalid Request', $validation['error']);
}

$action    = $validation['action'];
$bookingId = $validation['booking_id'];
$managerMobile = $validation['manager_mobile'];

// Action label map
$actionMap = [
    'av1' => ['label' => 'Available',           'status' => 'available'],
    'av0' => ['label' => 'Not Available',        'status' => 'not_available']
];
$meta = $actionMap[$action];

// -- Step 2: Check one-time use ----------------
if (isBookingAlreadyActioned($bookingId)) {
    logEvent('warn', 'Duplicate action attempt blocked', [
        'booking_id' => $bookingId,
        'manager_mobile' => $managerMobile,
        'action'     => $action,
        'ip'         => $_SERVER['REMOTE_ADDR'] ?? '',
    ]);
    renderPage(
        false,
        'Already Responded',
        'A response for this booking has already been submitted. No further changes can be made via this link.'
    );
}

// -- Step 3: Acquire one-time lock ------------
// markBookingActioned() re-checks inside an exclusive file lock
// to handle the race condition where two managers click simultaneously.
$locked = markBookingActioned($bookingId, $action, $managerMobile);

if (!$locked) {
    // Another request won the lock at the same instant
    logEvent('warn', 'Race condition blocked', [
        'booking_id' => $bookingId,
        'action'     => $action,
        'ip'         => $_SERVER['REMOTE_ADDR'] ?? '',
    ]);
    renderPage(
        false,
        'Already Responded',
        'A response for this booking has already been submitted. No further changes can be made via this link.'
    );
}

// -- Step 4: Update Magento --------------------
$result = updateMagentoBooking($bookingId, $meta['status'], $managerMobile);

if (!$result['success']) {
    // Log failure but do NOT release the lock.
    // The attempt is consumed - manager must contact support.
    // This prevents a failed Magento call from being retried
    // via repeated link clicks.
    logEvent('error', 'Magento update failed', [
        'booking_id' => $bookingId,
        'action'     => $action,
        'error'      => $result['data']['message'],
    ]);
    renderPage(
        false,
        'Update Failed',
        'Your response was received but the booking system could not be updated. Please contact support with Booking ID reference.'
    );
}

logEvent('info', 'Booking updated successfully', [
    'booking_id' => $bookingId,
    'action'     => $action,
    'status'     => $meta['status'],
]);

// -- Step 5: Show confirmation to manager -----
renderPage(
    true,
    'Response Submitted - ' . $meta['label'],
    'The booking status has been updated successfully in the system. Thank you for your response.'
);
