/**
 * Build the solitaire site and push it to a File Browser instance.
 *
 * Written against File Browser v2.63's REST API, read from its own bundle rather than guessed:
 *
 *   POST /api/login                          {username, password, recaptcha} -> JWT as plain text
 *   POST /api/resources/<dir>/               X-Auth: <jwt>                   -> create a directory
 *   POST /api/resources/<file>?override=true X-Auth: <jwt>, body = bytes     -> upload
 *
 * File Browser is the STORE, not the server — Start9 Pages serves the uploaded folder as the site.
 * So the job here is simply to land the built files in the right folder, intact.
 *
 * CREDENTIALS COME FROM THE ENVIRONMENT and are never written anywhere. Putting a password in a
 * repo is how it ends up in a commit, and this repo is going to be pushed.
 *
 *   FB_USER=jesse FB_PASS='…' npm run deploy:web
 *
 * Optional:
 *   FB_URL       default https://phoenix.local:58157
 *   FB_DEST      default websites/stationmaster — the folder Start9 Pages serves from
 *   FB_INSECURE  set to 1 for a self-signed certificate (usual for a .local StartOS host)
 *   --dry-run    list what would be sent, contact nothing
 */

import { execFileSync } from 'node:child_process';
import { readFileSync, readdirSync, statSync } from 'node:fs';
import { dirname, join, posix, relative, sep } from 'node:path';
import { fileURLToPath } from 'node:url';

const root = join(dirname(fileURLToPath(import.meta.url)), '..');
const dist = join(root, 'dist');

const URL_BASE = (process.env['FB_URL'] ?? 'https://phoenix.local:58157').replace(/\/+$/, '');
const DEST = `/${(process.env['FB_DEST'] ?? 'websites/stationmaster').replace(/^\/+|\/+$/g, '')}`;
const USER = process.env['FB_USER'] ?? '';
const PASS = process.env['FB_PASS'] ?? '';
const DRY = process.argv.includes('--dry-run');

/**
 * A .local StartOS host presents a certificate the system store does not know. Disabling
 * verification is opt-in and announced rather than silent: it is the right call on a LAN box you
 * own and the wrong one everywhere else, and that judgment is not the script's to make quietly.
 */
if (process.env['FB_INSECURE'] === '1') {
  process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0';
  console.warn('! TLS verification disabled (FB_INSECURE=1)');
}

/** Every file in `dir`, as paths relative to it, with POSIX separators. */
function walk(dir: string, base = dir): string[] {
  const out: string[] = [];
  for (const entry of readdirSync(dir)) {
    const full = join(dir, entry);
    if (statSync(full).isDirectory()) out.push(...walk(full, base));
    else out.push(relative(base, full).split(sep).join('/'));
  }
  return out.sort();
}

const CONTENT_TYPES: Record<string, string> = {
  '.html': 'text/html',
  '.js': 'text/javascript',
  '.css': 'text/css',
  '.json': 'application/json',
  '.txt': 'text/plain',
};

async function login(): Promise<string> {
  const res = await fetch(`${URL_BASE}/api/login`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ username: USER, password: PASS, recaptcha: '' }),
  });
  const body = await res.text();
  if (!res.ok) throw new Error(`login failed: ${res.status} ${body || res.statusText}`);
  if (!body.trim()) throw new Error('login returned an empty token');
  return body.trim();
}

async function makeDir(jwt: string, path: string): Promise<void> {
  // Trailing slash is what marks a directory in this API. A 409 means it already exists, which is
  // the normal case on every deploy after the first.
  const res = await fetch(`${URL_BASE}/api/resources${encodePath(path)}/`, {
    method: 'POST',
    headers: { 'X-Auth': jwt },
  });
  if (!res.ok && res.status !== 409) {
    throw new Error(`could not create ${path}: ${res.status} ${await res.text()}`);
  }
}

async function upload(jwt: string, localPath: string, remotePath: string): Promise<void> {
  const bytes = readFileSync(localPath);
  const ext = remotePath.slice(remotePath.lastIndexOf('.'));
  const res = await fetch(`${URL_BASE}/api/resources${encodePath(remotePath)}?override=true`, {
    method: 'POST',
    headers: {
      'X-Auth': jwt,
      'Content-Type': CONTENT_TYPES[ext] ?? 'application/octet-stream',
      'Content-Length': String(bytes.byteLength),
    },
    body: new Uint8Array(bytes),
  });
  if (!res.ok) throw new Error(`upload ${remotePath} failed: ${res.status} ${await res.text()}`);
}

/** Encode each segment but keep the separators, so a path stays a path. */
function encodePath(p: string): string {
  return p.split('/').map(encodeURIComponent).join('/');
}

// ---------------------------------------------------------------------------

console.log('building…');
execFileSync('node', ['scripts/build-web.ts'], { cwd: root, stdio: 'inherit' });

const files = walk(dist);
if (files.length === 0) throw new Error('dist/ is empty — nothing to deploy');

const dirs = [...new Set(files.map((f) => posix.dirname(f)).filter((d) => d !== '.'))].sort();
const total = files.reduce((n, f) => n + statSync(join(dist, f)).size, 0);

console.log(`\n${files.length} files, ${(total / 1024).toFixed(0)} KB`);
console.log(`  from ${dist}`);
console.log(`  to   ${URL_BASE}${DEST}\n`);

if (DRY) {
  for (const d of dirs) console.log(`  dir   ${DEST}/${d}`);
  for (const f of files) console.log(`  file  ${DEST}/${f}`);
  console.log('\ndry run — nothing was sent');
} else {
  if (!USER || !PASS) {
    throw new Error(
      'set FB_USER and FB_PASS.\n' +
        "  e.g.  FB_USER=me FB_PASS='…' FB_INSECURE=1 npm run deploy:web\n" +
        '  or run with --dry-run to see what would be sent',
    );
  }

  const jwt = await login();
  console.log('logged in');

  await makeDir(jwt, DEST);
  for (const d of dirs) await makeDir(jwt, `${DEST}/${d}`);

  let done = 0;
  for (const f of files) {
    await upload(jwt, join(dist, f), `${DEST}/${f}`);
    done++;
    console.log(`  [${String(done).padStart(2)}/${files.length}] ${f}`);
  }

  console.log(`\ndeployed to ${URL_BASE}${DEST}`);
  console.log('Start9 Pages serves these files as the site; File Browser is only the store.');
}
