/**
 * Import Wasabi IAM credentials into Passportal
 * Creates one credential per bucket entry under each client's existing Veeam subfolder
 *
 * Usage:
 *   npx tsx scripts/passportal-import-wasabi.ts --discover     # List templates, clients, folders
 *   npx tsx scripts/passportal-import-wasabi.ts --dry-run      # Preview what will be created
 *   npx tsx scripts/passportal-import-wasabi.ts                # Run the import
 */

import { createHmac } from 'crypto';
import { readFileSync } from 'fs';
import * as path from 'path';
import * as dotenv from 'dotenv';

dotenv.config({ path: path.resolve(process.cwd(), '.env') });

// ── Config ────────────────────────────────────────────────────────────────────

const BASE_URL = 'https://us-clover.passportalmsp.com';
const HMAC_CONTENT = 'aUa&&XUQBJXz2x&';
const VEEAM_FOLDER_NAME = 'Veeam'; // case-insensitive match
const CREDS_FILE = path.resolve(process.cwd(), 'dev/WasabiIAMCredentials_20260220_172028.txt');

// These Wulf-internal buckets have no matching client — skip them
const SKIP_CLIENT_SLUGS = new Set(['internal', 'vbr', 'veeam', 'clients']);

// ── Types ─────────────────────────────────────────────────────────────────────

interface WasabiEntry {
  bucket: string;
  username: string;
  accessKey: string;
  secretKey: string;
  endpoint: string;
  clientSlug: string;
}

interface PassportalToken {
  access_token: string;
  refresh_token: string;
  expiry_time: number;
}

interface PassportalClient {
  id: number | string;
  name: string;
}

interface PassportalFolder {
  id: number | string;
  name: string;
  clientId?: number | string;
}

interface PassportalTemplate {
  id: number | string;
  name: string;
  fields?: Array<{ name: string; type: string }>;
}

// ── Parse credentials file ────────────────────────────────────────────────────

function parseCredentialsFile(filePath: string): WasabiEntry[] {
  const content = readFileSync(filePath, 'utf-8');
  const entries: WasabiEntry[] = [];

  // Parse line-by-line to handle any line endings / blank-line variations
  const current: Record<string, string> = {};

  const flush = () => {
    const bucket    = current['bucket']     ?? '';
    const username  = current['username']   ?? '';
    const accessKey = current['access_key'] ?? '';
    const secretKey = current['secret_key'] ?? '';
    const endpoint  = current['endpoint']   || 'https://s3.wasabisys.com';

    if (bucket && accessKey && secretKey) {
      // wulf.<client>.veeam[365].immutable[1]  →  <client>
      const match = bucket.match(/^wulf\.(.+?)\.veeam/);
      const clientSlug = match ? match[1] : bucket;
      entries.push({ bucket, username, accessKey, secretKey, endpoint, clientSlug });
    }

    for (const k of Object.keys(current)) delete current[k];
  };

  for (const rawLine of content.split('\n')) {
    const line = rawLine.replace(/\r$/, ''); // strip CR for CRLF files

    if (line.trim() === '') {
      flush();
      continue;
    }

    const colonIdx = line.indexOf(':');
    if (colonIdx < 0) continue; // header separator lines (===)

    const rawKey = line.slice(0, colonIdx).trim().toLowerCase().replace(/\s+/g, '_');
    const value  = line.slice(colonIdx + 1).trim();

    // Skip header lines that don't look like credential fields
    if (!['bucket', 'username', 'access_key', 'secret_key', 'endpoint'].includes(rawKey)) continue;

    current[rawKey] = value;
  }

  flush(); // handle last block if file doesn't end with blank line

  return entries;
}

// ── Auth ──────────────────────────────────────────────────────────────────────

function computeHmac(secretKey: string): string {
  const hmac = createHmac('sha256', secretKey);
  hmac.update(HMAC_CONTENT);
  return hmac.digest('hex');
}

async function authenticate(scope = 'docs_api'): Promise<string> {
  const keyId  = process.env.ACCESS_KEY_ID;
  const secret = process.env.SECRET_ACCESS_KEY;

  if (!keyId || !secret) {
    throw new Error('ACCESS_KEY_ID and SECRET_ACCESS_KEY must be set in .env');
  }

  const hash = computeHmac(secret);

  const res = await fetch(`${BASE_URL}/api/v2/auth/client_token`, {
    method: 'POST',
    headers: {
      'x-key':        keyId,
      'x-hash':       hash,
      'content-type': 'application/json',
    },
    body: JSON.stringify({ scope, content: HMAC_CONTENT }),
  });

  if (!res.ok) {
    const text = await res.text();
    throw new Error(`Auth failed ${res.status}: ${text}`);
  }

  const data: PassportalToken = await res.json();
  return data.access_token;
}

// ── API helpers ───────────────────────────────────────────────────────────────

async function apiGet<T>(token: string, endpoint: string): Promise<T> {
  const res = await fetch(`${BASE_URL}/api/v2${endpoint}`, {
    headers: {
      'x-access-token': token,
      'content-type':   'application/json',
    },
  });

  if (!res.ok) {
    const text = await res.text();
    throw new Error(`GET ${endpoint} failed ${res.status}: ${text}`);
  }

  return res.json() as Promise<T>;
}

async function apiPost<T>(token: string, endpoint: string, body: unknown): Promise<T> {
  const res = await fetch(`${BASE_URL}/api/v2${endpoint}`, {
    method: 'POST',
    headers: {
      'x-access-token': token,
      'content-type':   'application/json',
    },
    body: JSON.stringify(body),
  });

  if (!res.ok) {
    const text = await res.text();
    throw new Error(`POST ${endpoint} failed ${res.status}: ${text}`);
  }

  return res.json() as Promise<T>;
}

// ── Client / folder matching ──────────────────────────────────────────────────

function decodeHtmlEntities(str: string): string {
  return str
    .replace(/&#0*39;/g, "'")
    .replace(/&amp;/g, '&')
    .replace(/&lt;/g, '<')
    .replace(/&gt;/g, '>')
    .replace(/&quot;/g, '"')
    .replace(/&#(\d+);/g, (_, n) => String.fromCharCode(Number(n)));
}

function slugify(name: string): string {
  return decodeHtmlEntities(name).toLowerCase().replace(/[^a-z0-9]/g, '');
}

function findClient(slug: string, clients: PassportalClient[]): PassportalClient | undefined {
  const target = slugify(slug);
  // Exact slug match first, then prefix match
  return (
    clients.find(c => slugify(c.name) === target) ||
    clients.find(c => slugify(c.name).includes(target)) ||
    clients.find(c => target.includes(slugify(c.name)))
  );
}

function findVeeamFolder(folders: PassportalFolder[]): PassportalFolder | undefined {
  return folders.find(f => f.name.toLowerCase().includes(VEEAM_FOLDER_NAME.toLowerCase()));
}

// ── Discover mode ─────────────────────────────────────────────────────────────

async function tryGet(token: string, label: string, endpoints: string[]): Promise<{ endpoint: string; data: unknown } | null> {
  for (const ep of endpoints) {
    try {
      const data = await apiGet<unknown>(token, ep);
      console.log(`  ✓ ${label} → ${ep}`);
      return { endpoint: ep, data };
    } catch (e) {
      console.log(`  ✗ ${ep}: ${(e as Error).message}`);
    }
  }
  return null;
}

interface PassportalDocument {
  id: number;
  client_id: number;
  clientName: string;
  templateId: number;
  templateName: string;
  type: string;
  label: string;
  folder_id?: number;
  folderName?: string;
  [key: string]: unknown;
}

async function fetchAllDocuments(token: string): Promise<PassportalDocument[]> {
  const all: PassportalDocument[] = [];
  let page = 1;

  while (true) {
    const res = await apiGet<{ results?: PassportalDocument[]; success?: boolean } | PassportalDocument[]>(
      token,
      `/documents?page=${page}&limit=100`
    );
    const batch = Array.isArray(res) ? res : (res as { results?: PassportalDocument[] }).results ?? [];
    all.push(...batch);
    if (batch.length < 100) break; // last page
    page++;
  }

  return all;
}

async function fetchFolders(token: string, clientId: number | string): Promise<PassportalFolder[]> {
  // The /folders endpoint requires the raw HMAC auth (x-key + x-hash), not the JWT
  const keyId  = process.env.ACCESS_KEY_ID!;
  const secret = process.env.SECRET_ACCESS_KEY!;
  const hash   = computeHmac(secret);

  for (const endpoint of [
    `/folders?clientId=${clientId}`,
    `/folders?client_id=${clientId}`,
  ]) {
    try {
      const res = await fetch(`${BASE_URL}/api/v2${endpoint}`, {
        headers: {
          'x-key':          keyId,
          'x-hash':         hash,
          'x-access-token': token,
          'content-type':   'application/json',
        },
      });
      if (!res.ok) continue;
      const data = await res.json() as { results?: PassportalFolder[] } | PassportalFolder[];
      return Array.isArray(data) ? data : (data as { results?: PassportalFolder[] }).results ?? [];
    } catch { /* try next */ }
  }
  return [];
}

async function runDiscover(token: string, entries: WasabiEntry[]) {
  console.log('\n=== FETCHING ALL DOCUMENTS ===');
  const docs = await fetchAllDocuments(token);
  console.log(`  Total documents: ${docs.length}`);

  // Unique templates
  const templates = new Map<number, { name: string; type: string }>();
  for (const d of docs) {
    if (!templates.has(d.templateId)) {
      templates.set(d.templateId, { name: d.templateName, type: d.type });
    }
  }
  console.log(`\n=== TEMPLATES (${templates.size} unique) ===`);
  for (const [id, t] of [...templates.entries()].sort((a, b) => a[0] - b[0])) {
    console.log(`  templateId=${id}  type=${t.type.padEnd(20)}  name="${t.name}"`);
  }

  // Look for folder fields in any document
  const docWithFolder = docs.find(d => d.folder_id || d.folderName);
  if (docWithFolder) {
    console.log('\n=== SAMPLE DOC WITH FOLDER FIELDS ===');
    console.log(JSON.stringify(docWithFolder, null, 2));
  } else {
    console.log('\n  No folder_id/folderName found in document list response');
  }

  // Try fetching a single document's full detail to see if it has more fields
  if (docs.length > 0) {
    console.log(`\n=== SINGLE DOCUMENT DETAIL (id=${docs[0].id}) ===`);
    try {
      const detail = await apiGet<unknown>(token, `/documents/${docs[0].id}`);
      console.log(JSON.stringify(detail, null, 2));
    } catch (e) {
      console.log('  Could not fetch detail:', (e as Error).message);
    }
  }

  // Unique clients from documents
  const clientMap = new Map<number, string>();
  for (const d of docs) {
    if (!clientMap.has(d.client_id)) clientMap.set(d.client_id, d.clientName);
  }
  console.log(`\n=== CLIENTS IN DOCUMENTS (${clientMap.size}) ===`);
  for (const [id, name] of [...clientMap.entries()].sort((a, b) => decodeHtmlEntities(a[1]).localeCompare(decodeHtmlEntities(b[1])))) {
    console.log(`  client_id=${id}  name="${decodeHtmlEntities(name)}"`);
  }

  // Try folders endpoint with both JWT and raw auth
  console.log('\n=== FOLDER ENDPOINT PROBE ===');
  if (clientMap.size > 0) {
    const firstClientId = [...clientMap.keys()][0];
    const folders = await fetchFolders(token, firstClientId);
    if (folders.length > 0) {
      console.log(`  ✓ Got ${folders.length} folder(s) for client ${firstClientId}:`);
      console.log(JSON.stringify(folders.slice(0, 5), null, 2));
    } else {
      console.log(`  ✗ No folders returned for client ${firstClientId}`);
      // Try raw endpoint probe
      const keyId  = process.env.ACCESS_KEY_ID!;
      const hash   = computeHmac(process.env.SECRET_ACCESS_KEY!);
      for (const ep of ['/folders', `/folders?clientId=${firstClientId}`, '/passwords', '/credentials']) {
        try {
          const res = await fetch(`${BASE_URL}/api/v2${ep}`, {
            headers: { 'x-key': keyId, 'x-hash': hash, 'content-type': 'application/json' },
          });
          const text = await res.text();
          console.log(`  raw-auth ${ep} → ${res.status}: ${text.slice(0, 200)}`);
        } catch (e) {
          console.log(`  raw-auth ${ep} → error: ${(e as Error).message}`);
        }
      }
    }
  }

  // Show client slug matching preview
  const slugs = [...new Set(entries.map(e => e.clientSlug))].filter(s => !SKIP_CLIENT_SLUGS.has(s));
  console.log(`\n=== CLIENT SLUG → CLIENT MATCH PREVIEW ===`);
  const clients: PassportalClient[] = [...clientMap.entries()].map(([id, name]) => ({ id, name }));
  for (const slug of slugs) {
    const match = findClient(slug, clients);
    if (match) {
      console.log(`  ✓ "${slug}" → "${decodeHtmlEntities(match.name)}" (id=${match.id})`);
    } else {
      console.log(`  ✗ "${slug}" → NO MATCH`);
    }
  }
}

// ── Main import ───────────────────────────────────────────────────────────────

async function runImport(token: string, entries: WasabiEntry[], dryRun: boolean) {
  // 1. Build client list from documents endpoint (client API returns 500)
  console.log('Fetching documents to build client list...');
  const allDocs = await fetchAllDocuments(token);
  console.log(`  Found ${allDocs.length} documents`);

  const clientMap = new Map<number, string>();
  for (const d of allDocs) {
    if (!clientMap.has(d.client_id)) clientMap.set(d.client_id, d.clientName);
  }
  const clients: PassportalClient[] = [...clientMap.entries()].map(([id, name]) => ({ id, name }));
  console.log(`  Found ${clients.length} unique clients`);

  // 2. Fetch templates and find best fit
  console.log('Fetching templates...');
  let templateUid: string | number | undefined;
  try {
    const res = await apiGet<unknown>(token, '/templates');
    const templates: PassportalTemplate[] = Array.isArray(res) ? res : ((res as { data?: PassportalTemplate[] }).data ?? []);
    // Prefer a template named something like "Username & Password" or "AWS" or "S3"
    const preferred = templates.find(t =>
      /username|password|credential|aws|s3|wasabi/i.test(t.name)
    ) ?? templates[0];
    if (preferred) {
      templateUid = preferred.id;
      console.log(`  Using template: "${preferred.name}" (${preferred.id})`);
    }
  } catch (e) {
    console.warn('  Could not fetch templates — templateUid will be omitted');
  }

  // 3. Group entries by client slug, skip internal buckets
  const bySlug = new Map<string, WasabiEntry[]>();
  for (const entry of entries) {
    if (SKIP_CLIENT_SLUGS.has(entry.clientSlug)) continue;
    const list = bySlug.get(entry.clientSlug) ?? [];
    list.push(entry);
    bySlug.set(entry.clientSlug, list);
  }

  const skipped = entries.filter(e => SKIP_CLIENT_SLUGS.has(e.clientSlug));
  if (skipped.length > 0) {
    console.log(`\nSkipping ${skipped.length} internal bucket(s): ${skipped.map(e => e.bucket).join(', ')}`);
  }

  // 4. Process each client slug
  let created = 0;
  let failed  = 0;
  const unmatched: string[] = [];

  for (const [slug, slugEntries] of bySlug) {
    const client = findClient(slug, clients);

    if (!client) {
      console.warn(`  ✗ No Passportal client found for slug "${slug}" — skipping ${slugEntries.length} entry/entries`);
      unmatched.push(slug);
      continue;
    }

    // Fetch folders for this client
    let folders: PassportalFolder[] = [];
    for (const endpoint of [
      `/folders?clientId=${client.id}`,
      `/clients/${client.id}/folders`,
    ]) {
      try {
        const res = await apiGet<unknown>(token, endpoint);
        folders = Array.isArray(res) ? res : ((res as { data?: PassportalFolder[] }).data ?? []);
        if (folders.length > 0) break;
      } catch { /* try next */ }
    }

    const veeamFolder = findVeeamFolder(folders);
    if (!veeamFolder) {
      console.warn(`  ✗ No "${VEEAM_FOLDER_NAME}" folder found for client "${client.name}" — skipping`);
      failed += slugEntries.length;
      continue;
    }

    // Create one credential per entry
    for (const entry of slugEntries) {
      const title = `Wasabi S3 - ${entry.bucket}`;
      const doc = {
        ...(templateUid !== undefined ? { templateUid } : {}),
        clientId: client.id,
        folderId: veeamFolder.id,
        title,
        // Common username/password fields (field names vary by template)
        username:    entry.accessKey,
        password:    entry.secretKey,
        // Extra context fields
        notes: [
          `Bucket:     ${entry.bucket}`,
          `IAM User:   ${entry.username}`,
          `Access Key: ${entry.accessKey}`,
          `Endpoint:   ${entry.endpoint}`,
        ].join('\n'),
        // Template-specific aliases
        access_key:   entry.accessKey,
        secret_key:   entry.secretKey,
        url:          entry.endpoint,
        application_name: `Wasabi S3 - ${entry.bucket}`,
      };

      if (dryRun) {
        console.log(`  [DRY RUN] Would create "${title}" under ${client.name} / ${veeamFolder.name}`);
      } else {
        try {
          await apiPost(token, '/documents', [doc]);
          console.log(`  ✓ Created "${title}" under ${client.name} / ${veeamFolder.name}`);
          created++;
        } catch (e) {
          console.error(`  ✗ Failed to create "${title}": ${(e as Error).message}`);
          failed++;
        }
      }
    }
  }

  // 5. Summary
  console.log('\n=== Summary ===');
  if (dryRun) {
    const total = [...bySlug.values()].reduce((n, v) => n + v.length, 0);
    console.log(`Would create: ${total} credentials`);
  } else {
    console.log(`Created: ${created}`);
    console.log(`Failed:  ${failed}`);
  }
  if (unmatched.length > 0) {
    console.log(`\nUnmatched client slugs (${unmatched.length}) — no Passportal client found:`);
    unmatched.forEach(s => console.log(`  - ${s}`));
    console.log('\nTip: run --discover to see the full client list and adjust matching manually.');
  }
}

// ── Entry point ───────────────────────────────────────────────────────────────

async function main() {
  const args = process.argv.slice(2);
  const isDiscover = args.includes('--discover');
  const isDryRun   = args.includes('--dry-run');

  console.log('=== Passportal Wasabi IAM Import ===');
  if (isDiscover) console.log('Mode: DISCOVER');
  else if (isDryRun) console.log('Mode: DRY RUN (no changes)');
  else console.log('Mode: LIVE IMPORT');

  const entries = parseCredentialsFile(CREDS_FILE);
  console.log(`\nParsed ${entries.length} credential entries from file`);

  const slugCounts = new Map<string, number>();
  for (const e of entries) {
    slugCounts.set(e.clientSlug, (slugCounts.get(e.clientSlug) ?? 0) + 1);
  }
  console.log(`Unique client slugs: ${[...slugCounts.keys()].join(', ')}`);

  console.log('\nAuthenticating...');
  const token = await authenticate();
  console.log('✓ Authenticated');

  if (isDiscover) {
    await runDiscover(token, entries);
  } else {
    await runImport(token, entries, isDryRun);
  }
}

main().catch(err => {
  console.error('\nFatal:', err.message);
  process.exit(1);
});
