<?php

require __DIR__ . '/../bootstrap.php';

use KeyLair\Auth;
use KeyLair\AuditLog;
use KeyLair\Csrf;
use KeyLair\Database;
use KeyLair\Platform;

// Contains real key values, so this is the one "view" page a reviewer
// (read-only, but should never see actual keys) can't reach at all.
Auth::requireEditAccess();
$admin = Auth::user();
$pdo = Database::connection();

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    die('This page only accepts POST requests.');
}
Csrf::requireValid();

// Optional filters, each independent of the others (product alone,
// platform alone, campaign alone, or any combination) -- Keys/Codes'
// own export form offers all three; keys_pool.php's still only ever
// sends product_id+platform together (fixed by the page it's on).
// With none given, exports across the whole install.
$productId = (int) ($_POST['product_id'] ?? 0);
$platform = $_POST['platform'] ?? '';
$campaignId = (int) ($_POST['campaign_id'] ?? 0);
// Kept specifically for the explicit-selection guard below, which is a
// security boundary (an explicit key_ids[] selection must still be
// scoped to one product+platform so a tampered id can't reach into a
// different product's keys) -- not the same thing as "any filter is
// active" now that filters can be independent.
$filtered = $productId > 0 && Platform::exists($platform);

// keys_pool.php can export an explicit checked selection instead of "every
// key matching the filters below" -- when given, this takes over entirely
// (still scoped to product_id+platform above, so a tampered id can't reach
// into a different product's keys).
$explicitIds = array_filter(array_map('intval', $_POST['key_ids'] ?? []));

// status=all includes already-claimed keys too, not just unused ones --
// still defaults to unused-only (status='available'), unchanged from
// before this option existed. Meaningless (ignored) for an explicit
// selection, which exports exactly what was checked regardless of status.
$allStatuses = ($_POST['status'] ?? '') === 'all';

// Caps how many rows are exported (e.g. "just give me 50 keys for this
// product+platform to hand off"), applied after the same ordering as an
// unlimited export so it's always the oldest-imported N, not an
// arbitrary subset. No cap by default. Meaningless for an explicit selection.
$limit = null;
if (isset($_POST['limit']) && ctype_digit((string) $_POST['limit']) && (int) $_POST['limit'] > 0) {
    $limit = (int) $_POST['limit'];
}

/**
 * Defense against CSV/formula injection: product, platform, and campaign
 * names are set by any admin, not just master admins, and this file can be opened
 * by a different admin in Excel/Sheets. A value starting with =, +, -, or @
 * would otherwise be interpreted as a formula there. Prefixing with a
 * single quote makes spreadsheet apps show it as plain text instead.
 */
function kv_csv_safe(?string $value): string
{
    $value = (string) $value;
    return preg_match('/^[=+\-@]/', $value) ? "'" . $value : $value;
}

$pdo->beginTransaction();
try {
    if (!empty($explicitIds)) {
        if (!$filtered) {
            throw new \RuntimeException('An explicit key selection requires product_id and platform.');
        }
        $placeholders = implode(',', array_fill(0, count($explicitIds), '?'));
        $lockStmt = $pdo->prepare(
            "SELECT k.id, p.name AS product_name, k.platform, k.key_value, k.status, k.pool, c.name AS campaign_name, k.created_at
             FROM `keys` k
             JOIN products p ON p.id = k.product_id
             LEFT JOIN campaigns c ON c.id = k.campaign_id
             WHERE k.product_id = ? AND k.platform = ? AND k.id IN ({$placeholders})
             ORDER BY k.created_at ASC
             FOR UPDATE"
        );
        $lockStmt->execute(array_merge([$productId, $platform], $explicitIds));
    } else {
        $where = [];
        $params = [];
        if ($productId > 0) {
            $where[] = 'k.product_id = :product_id';
            $params['product_id'] = $productId;
        }
        if ($platform !== '' && Platform::exists($platform)) {
            $where[] = 'k.platform = :platform';
            $params['platform'] = $platform;
        }
        if ($campaignId > 0) {
            $where[] = 'k.campaign_id = :campaign_id';
            $params['campaign_id'] = $campaignId;
        }
        $sql = "SELECT k.id, p.name AS product_name, k.platform, k.key_value, k.status, k.pool, c.name AS campaign_name, k.created_at
             FROM `keys` k
             JOIN products p ON p.id = k.product_id
             LEFT JOIN campaigns c ON c.id = k.campaign_id
             WHERE 1=1"
             . ($allStatuses ? '' : " AND k.status = 'available'")
             . ($where ? (' AND ' . implode(' AND ', $where)) : '')
             . ' ORDER BY p.name ASC, k.platform ASC, k.created_at ASC'
             . ($limit !== null ? " LIMIT {$limit}" : '')
             . ' FOR UPDATE';
        $lockStmt = $pdo->prepare($sql);
        $lockStmt->execute($params);
    }
    $rows = $lockStmt->fetchAll();

    // Only rows still 'available' actually get taken out of circulation --
    // an already-redeemed key (included when status=all, or handed over in
    // an explicit selection) was consumed through the real claim flow
    // already, so exporting it again for the record doesn't re-consume it.
    $availableIds = array_column(array_filter($rows, fn ($r) => $r['status'] === 'available'), 'id');
    if (!empty($availableIds)) {
        $placeholders = implode(',', array_fill(0, count($availableIds), '?'));
        $pdo->prepare("UPDATE `keys` SET status = 'exported' WHERE id IN ({$placeholders})")
            ->execute($availableIds);
    }

    $pdo->commit();
} catch (\Throwable $e) {
    $pdo->rollBack();
    http_response_code(500);
    die('Export failed: ' . $e->getMessage());
}

if (!empty($explicitIds)) {
    $filterDetail = 'selection count=' . count($explicitIds);
} else {
    $activeFilters = [];
    if ($productId > 0) {
        $activeFilters[] = "product={$productId}";
    }
    if ($platform !== '' && Platform::exists($platform)) {
        $activeFilters[] = "platform={$platform}";
    }
    if ($campaignId > 0) {
        $activeFilters[] = "campaign={$campaignId}";
    }
    $filterDetail = $activeFilters ? implode(' ', $activeFilters) : 'all';
}
$auditDetail = $filterDetail
    . ($allStatuses ? ' status=all' : '')
    . ($limit !== null ? " limit={$limit}" : '')
    . ' exported=' . count($availableIds);
AuditLog::log($admin['id'], 'keys_exported', $auditDetail);

$filename = ($allStatuses ? 'all' : 'unused') . '-keys-' . date('Y-m-d') . '.csv';

// Read by the calling page's JS to show a confirmation (count + filename
// only, never the key values themselves) without needing a second
// request -- the response here already IS the file download, which
// doesn't navigate the page, so there's nowhere else to put this.
header('X-Export-Summary: ' . rawurlencode(count($rows) . ' key(s) exported to ' . $filename));
header('Access-Control-Expose-Headers: X-Export-Summary');
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="' . $filename . '"');

$out = fopen('php://output', 'w');
fputcsv($out, ['Product', 'Platform', 'Key', 'Status', 'Release', 'Campaign', 'Imported at']);
foreach ($rows as $row) {
    fputcsv($out, [
        kv_csv_safe($row['product_name']),
        kv_csv_safe(Platform::label($row['platform'])),
        kv_csv_safe($row['key_value']),
        $row['status'] === 'available' ? 'Exported' : ucfirst($row['status']),
        $row['pool'] === 'active' ? 'Released' : 'Held back',
        kv_csv_safe($row['campaign_name']),
        $row['created_at'],
    ]);
}
fclose($out);
