"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SandboxController = void 0;
const common_1 = require("@nestjs/common");
const config_1 = require("@nestjs/config");
const swagger_1 = require("@nestjs/swagger");
const client_1 = require("@prisma/client");
const require_role_decorator_1 = require("../auth/require-role.decorator");
const api_paths_1 = require("../common/api-paths");
const swagger_docs_1 = require("../common/swagger-docs");
let SandboxController = class SandboxController {
    config;
    constructor(config) {
        this.config = config;
    }
    openSandbox() {
        const docsHref = (0, api_paths_1.docsPath)(this.config);
        const logoutAction = (0, api_paths_1.versionedApiPath)(this.config, 'console', 'logout');
        const apiBasePath = (0, api_paths_1.versionedApiPath)(this.config);
        return `<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Smart Estate API Sandbox</title>
  <style>
    :root {
      color-scheme: light;
      --bg: #f6f8fb;
      --panel: #ffffff;
      --ink: #172033;
      --muted: #607086;
      --line: #d9e1ec;
      --accent: #0f766e;
      --accent-strong: #0b5f59;
      --danger: #b42318;
      --code: #111827;
      --code-bg: #eef2f7;
    }
    * { box-sizing: border-box; }
    body {
      margin: 0;
      font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
      background: var(--bg);
      color: var(--ink);
    }
    header {
      padding: 24px 28px 18px;
      border-bottom: 1px solid var(--line);
      background: var(--panel);
    }
    h1 {
      margin: 0 0 6px;
      font-size: 24px;
      line-height: 1.2;
      letter-spacing: 0;
    }
    p { margin: 0; color: var(--muted); line-height: 1.5; }
    main {
      display: grid;
      grid-template-columns: minmax(280px, 360px) minmax(0, 1fr);
      gap: 18px;
      padding: 18px;
      max-width: 1440px;
      margin: 0 auto;
    }
    section, aside {
      background: var(--panel);
      border: 1px solid var(--line);
      border-radius: 8px;
      min-width: 0;
    }
    aside {
      padding: 14px;
      height: calc(100vh - 122px);
      overflow: auto;
      position: sticky;
      top: 12px;
    }
    .workspace {
      display: grid;
      gap: 18px;
      align-content: start;
    }
    .panel { padding: 16px; }
    label {
      display: block;
      font-size: 13px;
      font-weight: 700;
      margin: 0 0 6px;
    }
    input, select, textarea {
      width: 100%;
      border: 1px solid var(--line);
      border-radius: 6px;
      padding: 10px 11px;
      font: inherit;
      color: var(--ink);
      background: #fff;
    }
    textarea {
      min-height: 260px;
      resize: vertical;
      font-family: "Cascadia Code", Consolas, monospace;
      font-size: 13px;
      line-height: 1.45;
    }
    button {
      border: 0;
      border-radius: 6px;
      padding: 10px 13px;
      font: inherit;
      font-weight: 700;
      color: #fff;
      background: var(--accent);
      cursor: pointer;
    }
    button:hover { background: var(--accent-strong); }
    button.secondary {
      background: #e8eef6;
      color: var(--ink);
    }
    button.secondary:hover { background: #dce5f0; }
    .row {
      display: grid;
      grid-template-columns: repeat(2, minmax(0, 1fr));
      gap: 12px;
    }
    .stack { display: grid; gap: 12px; }
    .actions { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
    details {
      border: 1px solid var(--line);
      border-radius: 6px;
      padding: 10px;
      background: #fbfdff;
    }
    summary {
      cursor: pointer;
      font-weight: 800;
      font-size: 13px;
    }
    details .row { margin-top: 10px; }
    .endpoint-list {
      display: grid;
      gap: 8px;
      margin-top: 12px;
    }
    .endpoint {
      width: 100%;
      text-align: left;
      background: #f8fafc;
      color: var(--ink);
      border: 1px solid var(--line);
      padding: 9px;
      display: grid;
      gap: 4px;
    }
    .endpoint.active {
      border-color: var(--accent);
      outline: 2px solid rgba(15, 118, 110, .16);
    }
    .method {
      display: inline-block;
      width: max-content;
      padding: 2px 6px;
      border-radius: 4px;
      font-size: 11px;
      font-weight: 800;
      color: #fff;
      background: #334155;
    }
    .method.GET { background: #2563eb; }
    .method.POST { background: #0f766e; }
    .method.PATCH { background: #7c3aed; }
    .path {
      font-family: "Cascadia Code", Consolas, monospace;
      font-size: 12px;
      overflow-wrap: anywhere;
    }
    .hint {
      font-size: 12px;
      color: var(--muted);
      line-height: 1.4;
    }
    pre {
      margin: 0;
      padding: 14px;
      border-radius: 6px;
      background: var(--code-bg);
      color: var(--code);
      overflow: auto;
      min-height: 220px;
      font-family: "Cascadia Code", Consolas, monospace;
      font-size: 13px;
      line-height: 1.45;
    }
    .status {
      font-weight: 800;
      color: var(--muted);
    }
    .status.ok { color: var(--accent); }
    .status.error { color: var(--danger); }
    .token {
      font-family: "Cascadia Code", Consolas, monospace;
      font-size: 12px;
      overflow-wrap: anywhere;
      padding: 9px;
      border-radius: 6px;
      background: #f8fafc;
      border: 1px solid var(--line);
      color: var(--muted);
    }
    .token-input {
      min-height: 72px;
      max-height: 150px;
      resize: vertical;
      font-family: "Cascadia Code", Consolas, monospace;
      font-size: 12px;
      line-height: 1.4;
    }
    @media (max-width: 900px) {
      main { grid-template-columns: 1fr; }
      aside { position: static; height: auto; }
      .row { grid-template-columns: 1fr; }
    }
  </style>
</head>
<body>
  <header>
    <h1>Smart Estate API Sandbox</h1>
    <p>Use this super-admin sandbox to test OTP login, protected endpoints, request payloads, and raw API responses.</p>
  </header>
  <main>
    <aside>
      <label for="endpointSearch">Filter endpoints</label>
      <input id="endpointSearch" placeholder="auth, visitors, admin..." />
      <div id="endpointList" class="endpoint-list"></div>
    </aside>

    <div class="workspace">
      <section class="panel stack">
        <div class="row">
          <div>
            <label for="baseUrl">Base URL</label>
            <input id="baseUrl" />
          </div>
          <div>
            <label for="estateId">Estate ID</label>
            <input id="estateId" value="00000000-0000-4000-8000-000000000001" />
          </div>
        </div>
        <details>
          <summary>Runtime IDs</summary>
          <p class="hint">These are filled from list/create responses and used in placeholders like {residentId}, {guardId}, and {visitorPassId}.</p>
          <div class="row">
            <div>
              <label for="residentId">Resident ID</label>
              <input id="residentId" placeholder="Loaded from /residents" />
            </div>
            <div>
              <label for="guardId">Guard ID</label>
              <input id="guardId" placeholder="Loaded from /guards" />
            </div>
          </div>
          <div class="row">
            <div>
              <label for="visitorId">Visitor ID</label>
              <input id="visitorId" placeholder="Loaded from /visitors" />
            </div>
            <div>
              <label for="visitorPassId">Visitor Pass ID</label>
              <input id="visitorPassId" placeholder="Loaded from /visitors/passes" />
            </div>
          </div>
          <div class="row">
            <div>
              <label for="visitorCode">Visitor Code</label>
              <input id="visitorCode" placeholder="Loaded from pass response" />
            </div>
            <div>
              <label for="qrToken">QR Token</label>
              <input id="qrToken" placeholder="Loaded from pass response" />
            </div>
          </div>
          <div class="row">
            <div>
              <label for="deviceId">Scanner Device ID</label>
              <input id="deviceId" value="gate-tablet-001" />
            </div>
            <div>
              <label for="visitSessionId">Visit Session ID</label>
              <input id="visitSessionId" placeholder="Loaded from check-in response" />
            </div>
          </div>
          <div class="row">
            <div>
              <label for="userId">User ID</label>
              <input id="userId" placeholder="Loaded from admin/users" />
            </div>
          </div>
        </details>
        <div class="row">
          <div>
            <label for="phone">Seeded Phone</label>
            <select id="phone">
              <option value="+2348000000001">SUPER_ADMIN +2348000000001</option>
              <option value="+2348000000005">SALES_ADMIN +2348000000005</option>
              <option value="+2348000000002">ESTATE_ADMIN +2348000000002</option>
              <option value="+2348000000003">RESIDENT +2348000000003</option>
              <option value="+2348000000004">GUARD +2348000000004</option>
            </select>
          </div>
          <div>
            <label for="otp">OTP</label>
            <input id="otp" value="123456" />
          </div>
        </div>
        <div class="actions">
          <button id="loginBtn" type="button">Login</button>
          <button id="refreshTokenBtn" class="secondary" type="button">Refresh Token</button>
          <button id="loadIdsBtn" class="secondary" type="button">Load IDs</button>
          <button id="clearTokenBtn" class="secondary" type="button">Clear Tokens</button>
          <a href="${docsHref}" target="_blank">Open Swagger</a>
          <form method="post" action="${logoutAction}"><button class="secondary" type="submit">Console Logout</button></form>
        </div>
        <div>
          <label for="accessToken">Access Token</label>
          <textarea id="accessToken" class="token-input" spellcheck="false" placeholder="Paste access token here, with or without Bearer prefix"></textarea>
          <p id="tokenStatus" class="hint">No token stored.</p>
        </div>
        <div>
          <label for="refreshToken">Refresh Token</label>
          <textarea id="refreshToken" class="token-input" spellcheck="false" placeholder="Optional. Login fills this automatically."></textarea>
        </div>
      </section>

      <section class="panel stack">
        <div class="row">
          <div>
            <label for="method">Method</label>
            <select id="method">
              <option>GET</option>
              <option>POST</option>
              <option>PATCH</option>
            </select>
          </div>
          <div>
            <label for="path">Path</label>
            <input id="path" placeholder="/visitors" />
          </div>
        </div>
        <div>
          <label for="body">JSON Body</label>
          <textarea id="body">{}</textarea>
          <p class="hint">Path placeholders like {estateId} are replaced from the fields above. Leave body as {} for GET requests.</p>
        </div>
        <div class="actions">
          <button id="sendBtn" type="button">Send Request</button>
          <button id="formatBtn" class="secondary" type="button">Format JSON</button>
        </div>
      </section>

      <section class="panel stack">
        <div class="actions">
          <span id="responseStatus" class="status">No request sent.</span>
        </div>
        <pre id="responseOutput"></pre>
      </section>
    </div>
  </main>

  <script>
    const endpoints = [
      { group: 'Auth', method: 'POST', path: '/auth/login', title: 'Request OTP', body: { phone: '+2348000000001' } },
      { group: 'Auth', method: 'POST', path: '/auth/verify-otp', title: 'Verify OTP', body: { phone: '+2348000000001', otp: '123456' } },
      { group: 'Auth', method: 'POST', path: '/auth/refresh', title: 'Refresh tokens', body: { refreshToken: 'paste-refresh-token' } },
      { group: 'Auth', method: 'POST', path: '/auth/signup', title: 'Create account', body: { estateId: '{estateId}', name: 'Ade Okafor', phone: '+2348000000100', role: 'RESIDENT', unit: 'B14' } },
      { group: 'Features', method: 'GET', path: '/features/catalog', title: 'Feature catalog', body: {} },
      { group: 'Features', method: 'GET', path: '/features/me', title: 'My enabled features', body: {} },
      { group: 'Features', method: 'GET', path: '/features/role-settings?role=SALES_ADMIN', title: 'Sales admin feature settings', body: {} },
      { group: 'Features', method: 'PATCH', path: '/features/role-settings/SALES_ADMIN', title: 'Enable sales partner earnings/contact', body: { settings: [{ feature: 'SALES_PARTNER_EARNINGS_VIEW', enabled: true }, { feature: 'SALES_PARTNER_ADMIN_CONTACT', enabled: true }] } },
      { group: 'Features', method: 'PATCH', path: '/features/role-settings/SALES_ADMIN', title: 'Enable sales admin payment visibility', body: { settings: [{ feature: 'PAYMENTS_VIEW', enabled: true }, { feature: 'PAYMENT_PROVIDERS_VIEW', enabled: true }] } },
      { group: 'Features', method: 'PATCH', path: '/features/role-settings/GUARD', title: 'Disable guard manual check-in for estate', body: { estateId: '{estateId}', settings: [{ feature: 'CHECKIN_MANUAL', enabled: false }] } },
      { group: 'Onboarding', method: 'GET', path: '/onboarding/subscription-plans', title: 'List subscription plans', body: {} },
      { group: 'Onboarding', method: 'GET', path: '/onboarding/payment-providers?estateId={estateId}', title: 'List payment providers', body: {} },
      { group: 'Onboarding', method: 'POST', path: '/onboarding/estate-admin-signup', title: 'Estate admin lead signup', body: { estateName: 'GreenPark Estate', location: 'Lagos, Nigeria', unitCount: 120, estateType: 'Residential gated community', adminName: 'Ade Okafor', adminPhone: '+2348000000990', adminEmail: 'estate.admin@example.com', metadata: { source: 'sandbox' } } },
      { group: 'Onboarding', method: 'POST', path: '/onboarding/estates/{estateId}/select-plan', title: 'Select plan', body: { adminPhone: '+2348000000990', planCode: 'SECURE', paymentProvider: 'FLUTTERWAVE' } },
      { group: 'Onboarding', method: 'POST', path: '/onboarding/estates/{estateId}/payments/initialize', title: 'Initialize hosted checkout', body: { adminPhone: '+2348000000990', customerEmail: 'estate.admin@example.com', customerName: 'Ade Okafor', planCode: 'SECURE', paymentProvider: 'FLUTTERWAVE', metadata: { channel: 'card' } } },
      { group: 'Onboarding', method: 'GET', path: '/onboarding/payments/callback?provider=FLUTTERWAVE&reference=FLU-1778881112222-a1b2c3d4&transaction_id=123456789', title: 'Payment callback example', body: {} },
      { group: 'Onboarding', method: 'POST', path: '/onboarding/estates/{estateId}/payments', title: 'Record manual payment reference', body: { adminPhone: '+2348000000990', reference: 'FLU-1778881112222-a1b2c3d4', metadata: { channel: 'manual' } } },
      { group: 'Estates', method: 'GET', path: '/estates', title: 'List estates', body: {} },
      { group: 'Estates', method: 'POST', path: '/estates', title: 'Create estate', body: { name: 'GreenPark Estate', location: 'Lagos, Nigeria', settings: { timezone: 'Africa/Lagos' }, branding: { primaryColor: '#0f766e' } } },
      { group: 'Residents', method: 'GET', path: '/residents?estateId={estateId}&q=Ade', title: 'Search residents', body: {} },
      { group: 'Residents', method: 'POST', path: '/residents', title: 'Create resident', body: { estateId: '{estateId}', name: 'Ade Okafor', phone: '+2348000000300', unit: 'A12' } },
      { group: 'Guards', method: 'GET', path: '/guards?estateId={estateId}', title: 'List guards', body: {} },
      { group: 'Guards', method: 'POST', path: '/guards', title: 'Create guard', body: { estateId: '{estateId}', name: 'Ade Okafor', phone: '+2348000000200', deviceId: 'guard-device-002' } },
      { group: 'Visitors', method: 'GET', path: '/visitors?estateId={estateId}&q=Ade', title: 'Search visitors', body: {} },
      { group: 'Visitors', method: 'POST', path: '/visitors', title: 'Create visitor', body: { estateId: '{estateId}', name: 'Ade Okafor', phone: '+2348111111111', vehiclePlate: 'LND-123-AB' } },
      { group: 'Visitors', method: 'GET', path: '/visitors/passes?estateId={estateId}', title: 'List passes', body: {} },
      { group: 'Visitors', method: 'POST', path: '/visitors/passes', title: 'Create pass', body: { estateId: '{estateId}', visitorId: '{visitorId}', hostResidentId: '{residentId}', passType: 'SINGLE_USE', validFrom: new Date().toISOString(), validUntil: new Date(Date.now() + 3600000).toISOString(), maxEntries: 1 } },
      { group: 'Visitors', method: 'POST', path: '/visitors/passes/{visitorPassId}/revoke', title: 'Revoke pass', body: {} },
      { group: 'Checkin', method: 'POST', path: '/checkin/scan', title: 'QR check-in', body: { estateId: '{estateId}', qrToken: '{qrToken}', guardId: '{guardId}', deviceId: '{deviceId}', entryGate: 'Main Gate' } },
      { group: 'Checkin', method: 'POST', path: '/checkin/manual', title: 'Manual check-in', body: { estateId: '{estateId}', visitorCode: '{visitorCode}', guardId: '{guardId}', deviceId: '{deviceId}', entryGate: 'Main Gate' } },
      { group: 'Checkin', method: 'POST', path: '/checkin/checkout', title: 'Check out', body: { visitSessionId: '{visitSessionId}', guardId: '{guardId}', deviceId: '{deviceId}', exitGate: 'Main Gate' } },
      { group: 'Incidents', method: 'GET', path: '/incidents?estateId={estateId}', title: 'List incidents', body: {} },
      { group: 'Incidents', method: 'POST', path: '/incidents', title: 'Create incident', body: { estateId: '{estateId}', guardId: '{guardId}', type: 'SECURITY', description: 'Suspicious activity near the main gate', metadata: { gate: 'Main Gate' } } },
      { group: 'Offline', method: 'GET', path: '/offline/bootstrap?estateId={estateId}', title: 'Offline bootstrap', body: {} },
      { group: 'Offline', method: 'POST', path: '/offline/sync', title: 'Sync offline events', body: { estateId: '{estateId}', deviceId: 'guard-device-001', items: [{ clientEventId: 'offline-event-001', eventType: 'VISITOR_CHECKED_IN', payload: { visitorCode: 'ABC12345' } }] } },
      { group: 'Notifications', method: 'POST', path: '/notifications/send', title: 'Send notification', body: { estateId: '{estateId}', channel: 'SMS', recipient: '+2348111111111', message: 'Your visitor pass is ready', metadata: { source: 'sandbox' } } },
      { group: 'Admin', method: 'GET', path: '/admin/dashboard', title: 'Dashboard summary', body: {} },
      { group: 'Admin', method: 'GET', path: '/admin/otp/config', title: 'OTP provider config', body: {} },
      { group: 'Admin', method: 'PATCH', path: '/admin/otp/config', title: 'Switch OTP provider', body: { provider: 'DEV', enabledProviders: ['DEV'], providerPriority: ['DEV', 'TERMII', 'AFRICAS_TALKING', 'CUSTOM_HTTP'], ttlSeconds: 300, codeLength: 6, maxAttempts: 5, exposeDevCode: true, termiiBaseUrl: 'https://api.ng.termii.com', termiiSenderId: 'SmartEstate', termiiChannel: 'dnd', africasTalkingBaseUrl: 'https://api.africastalking.com/version1/messaging', africasTalkingUsername: 'sandbox', customHttpUrl: 'https://sms.example.com/send', customHttpMethod: 'POST', customHttpHeadersJson: '{"Authorization":"Bearer sms_api_key"}', customHttpBodyTemplate: '{"to":"{{phone}}","message":"{{message}}","code":"{{otp}}"}' } },
      { group: 'Admin', method: 'GET', path: '/admin/estate-signups?status=PENDING_APPROVAL', title: 'List estate signups', body: {} },
      { group: 'Admin', method: 'PATCH', path: '/admin/estate-signups/{estateId}/review', title: 'Review estate signup', body: { status: 'APPROVED', reviewNotes: 'Approved for subscription plan selection.' } },
      { group: 'Admin', method: 'PATCH', path: '/admin/estate-signups/{estateId}/confirm-payment', title: 'Confirm estate payment', body: { paymentReference: 'FLU-1778881112222-a1b2c3d4' } },
      { group: 'Admin', method: 'GET', path: '/admin/subscription-plans?includeInactive=true', title: 'Manage plans', body: {} },
      { group: 'Admin', method: 'POST', path: '/admin/subscription-plans', title: 'Create custom plan', body: { code: 'CUSTOM_LUXURY', name: 'Luxury Custom', minResidents: 301, maxResidents: null, monthlyPriceKobo: 150000000, currency: 'NGN', customPricing: true, description: 'Custom plan controlled by super admin.', isActive: true, features: ['RESIDENTS_VIEW', 'GUARDS_VIEW', 'VISITORS_VIEW', 'VISITOR_PASSES_CREATE', 'CHECKIN_SCAN', 'INCIDENTS_VIEW'] } },
      { group: 'Admin', method: 'PATCH', path: '/admin/subscription-plans/STARTER', title: 'Update plan features and price', body: { monthlyPriceKobo: 20000000, isActive: true, features: ['RESIDENTS_VIEW', 'VISITORS_VIEW', 'VISITOR_PASSES_CREATE', 'CHECKIN_SCAN'] } },
      { group: 'Admin', method: 'GET', path: '/admin/sales-partners', title: 'List sales partners', body: {} },
      { group: 'Admin', method: 'GET', path: '/admin/sales-partners/me', title: 'My sales partner profile', body: {} },
      { group: 'Admin', method: 'GET', path: '/admin/sales-partners/me/earnings', title: 'My partner earnings', body: {} },
      { group: 'Admin', method: 'GET', path: '/admin/sales-partners/{userId}/earnings', title: 'Sales partner earnings by ID', body: {} },
      { group: 'Admin', method: 'PATCH', path: '/admin/sales-partners/{userId}', title: 'Enable partner earnings', body: { enabled: true, commissionPercent: 7.5, notes: 'Partner status enabled after super-admin review.' } },
      { group: 'Admin', method: 'PATCH', path: '/admin/estates/{estateId}/sales-partner', title: 'Assign estate sales partner', body: { salesAdminId: '{userId}' } },
      { group: 'Admin', method: 'POST', path: '/admin/estates/{estateId}/admin-contact', title: 'Contact estate admins', body: { channel: 'SMS', message: 'Please complete your subscription payment so we can activate your estate.', metadata: { reason: 'PAYMENT_PENDING' } } },
      { group: 'Admin', method: 'GET', path: '/admin/payment-providers?estateId={estateId}', title: 'Provider settings', body: {} },
      { group: 'Admin', method: 'PATCH', path: '/admin/payment-providers/FLUTTERWAVE', title: 'Toggle global provider', body: { enabled: true } },
      { group: 'Admin', method: 'PATCH', path: '/admin/estates/{estateId}/payment-providers/PAYSTACK', title: 'Toggle estate provider', body: { enabled: true } },
      { group: 'Admin', method: 'GET', path: '/admin/users?estateId={estateId}', title: 'List users', body: {} },
      { group: 'Admin', method: 'PATCH', path: '/admin/users/{userId}', title: 'Update user', body: { status: 'SUSPENDED' } },
      { group: 'Admin', method: 'GET', path: '/admin/audit-logs?estateId={estateId}', title: 'Audit logs', body: {} }
    ];

    const $ = (id) => document.getElementById(id);
    const baseUrl = $('baseUrl');
    const method = $('method');
    const path = $('path');
    const body = $('body');
    const accessToken = $('accessToken');
    const refreshToken = $('refreshToken');
    const tokenStatus = $('tokenStatus');
    const responseStatus = $('responseStatus');
    const responseOutput = $('responseOutput');
    const runtimeIdFields = ['estateId', 'residentId', 'guardId', 'visitorId', 'visitorPassId', 'visitSessionId', 'visitorCode', 'qrToken', 'deviceId', 'userId'];

    baseUrl.value = window.location.origin + '${apiBasePath}';

    function cleanToken(value) {
      return String(value || '')
        .trim()
        .replace(/^Bearer\\s+/i, '')
        .replace(/^["']+|["']+$/g, '')
        .trim();
    }

    function token() {
      return cleanToken(accessToken.value);
    }

    function refreshTokenValue() {
      return cleanToken(refreshToken.value);
    }

    function decodeJwt(tokenValue) {
      try {
        const payload = tokenValue.split('.')[1];
        if (!payload) return null;
        const normalized = payload.replace(/-/g, '+').replace(/_/g, '/');
        const padded = normalized + '='.repeat((4 - normalized.length % 4) % 4);
        return JSON.parse(atob(padded));
      } catch (_) {
        return null;
      }
    }

    function isTokenExpired(tokenValue, skewSeconds) {
      const payload = decodeJwt(tokenValue);
      if (!payload || !payload.exp) return false;
      return payload.exp * 1000 <= Date.now() + (skewSeconds || 0) * 1000;
    }

    function saveTokens() {
      const access = token();
      const refresh = refreshTokenValue();
      if (access) localStorage.setItem('smartEstateAccessToken', access);
      else localStorage.removeItem('smartEstateAccessToken');
      if (refresh) localStorage.setItem('smartEstateRefreshToken', refresh);
      else localStorage.removeItem('smartEstateRefreshToken');
      updateTokenStatus();
    }

    function loadTokens() {
      accessToken.value = localStorage.getItem('smartEstateAccessToken') || '';
      refreshToken.value = localStorage.getItem('smartEstateRefreshToken') || '';
      updateTokenStatus();
    }

    function loadRuntimeIds() {
      runtimeIdFields.forEach((id) => {
        const field = $(id);
        if (!field) return;
        const stored = localStorage.getItem('smartEstateSandbox_' + id);
        if (stored) field.value = stored;
      });
    }

    function saveRuntimeIds() {
      runtimeIdFields.forEach((id) => {
        const field = $(id);
        if (!field) return;
        const value = field.value.trim();
        if (value) localStorage.setItem('smartEstateSandbox_' + id, value);
        else localStorage.removeItem('smartEstateSandbox_' + id);
      });
    }

    function setRuntimeId(id, value, overwrite) {
      if (!value) return;
      const field = $(id);
      if (!field) return;
      if (!overwrite && field.value.trim()) return;
      field.value = String(value);
      localStorage.setItem('smartEstateSandbox_' + id, String(value));
    }

    function updateTokenStatus() {
      const value = token();
      const payload = decodeJwt(value);
      if (!value) {
        tokenStatus.textContent = 'No token stored.';
        return;
      }
      if (!payload) {
        tokenStatus.textContent = 'Token is present but is not a valid JWT shape.';
        return;
      }
      const expiresAt = payload.exp ? new Date(payload.exp * 1000).toLocaleString() : 'unknown expiry';
      const expired = isTokenExpired(value, 0);
      tokenStatus.textContent =
        (expired ? 'Expired' : 'Active') +
        ' access token for ' +
        (payload.role || 'unknown role') +
        ', estate ' +
        (payload.estateId || 'none') +
        ', expires ' +
        expiresAt +
        '.';
    }

    function replaceTokens(value) {
      return runtimeIdFields.reduce(
        (current, id) => current.replaceAll('{' + id + '}', ($(id)?.value || '').trim()),
        value
      );
    }

    function captureIds(payload) {
      const seen = Array.isArray(payload) ? payload : [payload];
      seen.forEach(captureIdsFromObject);
    }

    function captureIdsFromObject(item) {
      if (!item || typeof item !== 'object' || Array.isArray(item)) return;

      if (item.estate?.id) setRuntimeId('estateId', item.estate.id, false);
      if (item.user?.id) setRuntimeId('userId', item.user.id, false);
      if (item.resident?.id) setRuntimeId('residentId', item.resident.id, true);
      if (item.guard?.id) setRuntimeId('guardId', item.guard.id, true);
      if (item.visitor?.id) setRuntimeId('visitorId', item.visitor.id, true);
      if (item.visitorPass?.id) setRuntimeId('visitorPassId', item.visitorPass.id, true);

      if (item.role && item.id) setRuntimeId('userId', item.id, false);
      if (item.unit && item.userId && item.id) setRuntimeId('residentId', item.id, true);
      if (Object.prototype.hasOwnProperty.call(item, 'deviceId') && item.userId && item.id) setRuntimeId('guardId', item.id, true);
      if (Object.prototype.hasOwnProperty.call(item, 'vehiclePlate') && item.id) setRuntimeId('visitorId', item.id, true);
      if ((item.visitorCode || item.qrToken || item.qrPayload) && item.id) setRuntimeId('visitorPassId', item.id, true);
      if (item.visitorPassId && item.guardId && item.id) setRuntimeId('visitSessionId', item.id, true);

      if (item.userId) setRuntimeId('userId', item.userId, false);
      if (item.visitorId) setRuntimeId('visitorId', item.visitorId, true);
      if (item.hostResidentId) setRuntimeId('residentId', item.hostResidentId, true);
      if (item.guardId) setRuntimeId('guardId', item.guardId, true);
      if (item.visitorPassId) setRuntimeId('visitorPassId', item.visitorPassId, true);
      if (item.deviceId) setRuntimeId('deviceId', item.deviceId, false);
      if (item.visitorCode) setRuntimeId('visitorCode', item.visitorCode, true);
      if (item.qrToken) setRuntimeId('qrToken', item.qrToken, true);
      if (item.qrPayload?.token) setRuntimeId('qrToken', item.qrPayload.token, true);
    }

    function selectEndpoint(endpoint, button) {
      document.querySelectorAll('.endpoint').forEach((item) => item.classList.remove('active'));
      if (button) button.classList.add('active');
      method.value = endpoint.method;
      path.value = replaceTokens(endpoint.path);
      body.value = JSON.stringify(JSON.parse(replaceTokens(JSON.stringify(endpoint.body))), null, 2);
    }

    function renderEndpoints() {
      const query = $('endpointSearch').value.trim().toLowerCase();
      const list = $('endpointList');
      list.innerHTML = '';
      endpoints
        .filter((endpoint) => [endpoint.group, endpoint.method, endpoint.path, endpoint.title].join(' ').toLowerCase().includes(query))
        .forEach((endpoint) => {
          const button = document.createElement('button');
          button.type = 'button';
          button.className = 'endpoint';
          button.innerHTML = '<span class="method ' + endpoint.method + '">' + endpoint.method + '</span><span class="path">' + endpoint.path + '</span><span class="hint">' + endpoint.group + ' - ' + endpoint.title + '</span>';
          button.addEventListener('click', () => selectEndpoint(endpoint, button));
          list.appendChild(button);
        });
    }

    async function refreshAccessToken(showResult) {
      const refresh = refreshTokenValue();
      if (!refresh) {
        updateTokenStatus();
        if (showResult) {
          responseStatus.className = 'status error';
          responseStatus.textContent = 'No refresh token available';
          responseOutput.textContent = 'Log in again or paste a refresh token.';
        }
        return '';
      }

      const root = baseUrl.value.replace(/\\/$/, '');
      try {
        const response = await fetch(root + '/auth/refresh', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
          body: JSON.stringify({ refreshToken: refresh })
        });
        const payload = await response.json();
        if (!response.ok) throw new Error(JSON.stringify(payload, null, 2));

        accessToken.value = cleanToken(payload.accessToken);
        refreshToken.value = cleanToken(payload.refreshToken);
        saveTokens();

        if (showResult) {
          responseStatus.className = 'status ok';
          responseStatus.textContent = 'Token refreshed';
          responseOutput.textContent = JSON.stringify(payload, null, 2);
        }
        return token();
      } catch (error) {
        updateTokenStatus();
        if (showResult) {
          responseStatus.className = 'status error';
          responseStatus.textContent = 'Refresh failed';
          responseOutput.textContent = String(error);
        }
        return '';
      }
    }

    async function accessTokenForRequest() {
      const access = token();
      if (!access) return '';
      if (isTokenExpired(access, 30)) {
        return await refreshAccessToken(false);
      }
      return access;
    }

    async function sendRequest() {
      responseStatus.className = 'status';
      responseStatus.textContent = 'Sending...';
      responseOutput.textContent = '';

      let parsedBody = {};
      try {
        parsedBody = body.value.trim() ? JSON.parse(body.value) : {};
      } catch (error) {
        responseStatus.className = 'status error';
        responseStatus.textContent = 'Invalid JSON body';
        responseOutput.textContent = String(error);
        return;
      }

      const headers = { Accept: 'application/json' };
      if (method.value !== 'GET') headers['Content-Type'] = 'application/json';
      const access = await accessTokenForRequest();
      if (access) headers.Authorization = 'Bearer ' + access;

      const url = baseUrl.value.replace(/\\/$/, '') + replaceTokens(path.value);
      const options = { method: method.value, headers };
      if (method.value !== 'GET') options.body = JSON.stringify(parsedBody);

      const started = performance.now();
      try {
        let response = await fetch(url, options);
        if (response.status === 401 && headers.Authorization && refreshTokenValue() && !url.includes('/auth/refresh')) {
          const refreshed = await refreshAccessToken(false);
          if (refreshed) {
            headers.Authorization = 'Bearer ' + refreshed;
            response = await fetch(url, options);
          }
        }
        const text = await response.text();
        let payload = text;
        try { payload = JSON.parse(text); } catch (_) {}
        if (response.ok && typeof payload !== 'string') captureIds(payload);
        responseStatus.className = 'status ' + (response.ok ? 'ok' : 'error');
        responseStatus.textContent = response.status + ' ' + response.statusText + ' in ' + Math.round(performance.now() - started) + 'ms';
        responseOutput.textContent = typeof payload === 'string' ? payload : JSON.stringify(payload, null, 2);
      } catch (error) {
        responseStatus.className = 'status error';
        responseStatus.textContent = 'Request failed';
        responseOutput.textContent = String(error);
      }
    }

    async function login() {
      const phone = $('phone').value;
      const otp = $('otp').value;
      const root = baseUrl.value.replace(/\\/$/, '');
      responseStatus.className = 'status';
      responseStatus.textContent = 'Logging in...';
      responseOutput.textContent = '';
      try {
        await fetch(root + '/auth/login', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
          body: JSON.stringify({ phone })
        });
        const response = await fetch(root + '/auth/verify-otp', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
          body: JSON.stringify({ phone, otp })
        });
        const payload = await response.json();
        if (!response.ok) throw new Error(JSON.stringify(payload, null, 2));
        accessToken.value = cleanToken(payload.accessToken);
        refreshToken.value = cleanToken(payload.refreshToken);
        saveTokens();
        responseStatus.className = 'status ok';
        responseStatus.textContent = 'Logged in and stored access token';
        responseOutput.textContent = JSON.stringify(payload, null, 2);
      } catch (error) {
        responseStatus.className = 'status error';
        responseStatus.textContent = 'Login failed';
        responseOutput.textContent = String(error);
      }
    }

    async function loadIds() {
      responseStatus.className = 'status';
      responseStatus.textContent = 'Loading IDs...';
      responseOutput.textContent = '';

      const access = await accessTokenForRequest();
      if (!access) {
        responseStatus.className = 'status error';
        responseStatus.textContent = 'Login required';
        responseOutput.textContent = 'Log in or paste an access token before loading IDs.';
        return;
      }

      const headers = { Accept: 'application/json', Authorization: 'Bearer ' + access };
      const root = baseUrl.value.replace(/\\/$/, '');
      const pathsToLoad = [
        '/residents?estateId={estateId}',
        '/guards?estateId={estateId}',
        '/visitors?estateId={estateId}',
        '/visitors/passes?estateId={estateId}',
        '/admin/users?estateId={estateId}'
      ];
      const report = [];

      for (const itemPath of pathsToLoad) {
        const url = root + replaceTokens(itemPath);
        try {
          const response = await fetch(url, { headers });
          const text = await response.text();
          let payload = text;
          try { payload = JSON.parse(text); } catch (_) {}
          if (response.ok && typeof payload !== 'string') captureIds(payload);
          report.push({ path: itemPath, status: response.status, ok: response.ok });
        } catch (error) {
          report.push({ path: itemPath, error: String(error) });
        }
      }

      responseStatus.className = 'status ok';
      responseStatus.textContent = 'ID loading complete';
      responseOutput.textContent = JSON.stringify({
        loaded: report,
        ids: Object.fromEntries(runtimeIdFields.map((id) => [id, ($(id)?.value || '').trim()]))
      }, null, 2);
    }

    $('endpointSearch').addEventListener('input', renderEndpoints);
    runtimeIdFields.forEach((id) => $(id)?.addEventListener('input', saveRuntimeIds));
    accessToken.addEventListener('input', saveTokens);
    accessToken.addEventListener('blur', () => {
      accessToken.value = token();
      saveTokens();
    });
    refreshToken.addEventListener('input', saveTokens);
    refreshToken.addEventListener('blur', () => {
      refreshToken.value = refreshTokenValue();
      saveTokens();
    });
    $('sendBtn').addEventListener('click', sendRequest);
    $('loginBtn').addEventListener('click', login);
    $('refreshTokenBtn').addEventListener('click', () => refreshAccessToken(true));
    $('loadIdsBtn').addEventListener('click', loadIds);
    $('clearTokenBtn').addEventListener('click', () => {
      localStorage.removeItem('smartEstateAccessToken');
      localStorage.removeItem('smartEstateRefreshToken');
      accessToken.value = '';
      refreshToken.value = '';
      updateTokenStatus();
    });
    $('formatBtn').addEventListener('click', () => {
      body.value = JSON.stringify(JSON.parse(body.value || '{}'), null, 2);
    });

    renderEndpoints();
    loadRuntimeIds();
    loadTokens();
    selectEndpoint(endpoints[0], null);
  </script>
</body>
</html>`;
    }
};
exports.SandboxController = SandboxController;
__decorate([
    (0, common_1.Get)(),
    (0, common_1.Header)('Content-Type', 'text/html; charset=utf-8'),
    (0, swagger_1.ApiOperation)({
        summary: 'Open the local API testing sandbox',
        description: 'Serves a lightweight browser-based API sandbox for testing authentication, protected routes, request bodies, and responses against the local API.'
    }),
    (0, swagger_1.ApiResponse)({
        status: 200,
        description: 'HTML sandbox page returned.'
    }),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", []),
    __metadata("design:returntype", void 0)
], SandboxController.prototype, "openSandbox", null);
exports.SandboxController = SandboxController = __decorate([
    (0, swagger_1.ApiTags)('Sandbox'),
    (0, swagger_docs_1.ApiProtectedEndpoint)(),
    (0, require_role_decorator_1.RequireRole)(client_1.UserRole.SUPER_ADMIN),
    (0, common_1.Controller)({ path: 'sandbox', version: '1' }),
    __metadata("design:paramtypes", [config_1.ConfigService])
], SandboxController);
//# sourceMappingURL=sandbox.controller.js.map