// ─────────────────────────────────────────────────────────────────────────
// Managed by Ghosty — platform plumbing. Don't edit this file; import its
// functions from your routes/services instead.
// ─────────────────────────────────────────────────────────────────────────

import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';

/**
 * File storage for this app — a private place for uploads, images, documents
 * and generated files. Optional, like the database: enable it by setting
 * `"fileStorage": true` in app.config.json and running `ghosty provision`.
 *
 * The same five functions work everywhere:
 *  - Locally (`ghosty dev`) files live in a `.data/files` folder next to the
 *    backend, so development needs no setup and no credentials.
 *  - In the published app, Ghosty provides managed file storage and this
 *    module talks to it directly — no extra packages, no credentials in the
 *    code; the app's own platform identity does the work.
 *
 * Everything stored here is PRIVATE to the app. To let someone download a
 * file without signing in, hand them a share link (`shareLink`) — it expires
 * on its own.
 */

const LOCAL_ROOT = path.resolve(__dirname, '..', '..', '.data', 'files');

function bucketName(): string | undefined {
  const name = process.env.FILE_STORAGE_BUCKET;
  return name && name.length > 0 ? name : undefined;
}

/** Reject names that would escape the storage root ('../', absolute paths). */
function safeName(name: string): string {
  const normalized = name.replace(/\\/g, '/').replace(/^\/+/, '');
  const resolved = path.posix.normalize(normalized);
  if (resolved.startsWith('..') || path.posix.isAbsolute(resolved) || resolved === '.') {
    throw new Error(`"${name}" is not a valid file name.`);
  }
  return resolved;
}

function localPath(name: string): string {
  return path.join(LOCAL_ROOT, ...safeName(name).split('/'));
}

// ── Platform storage plumbing (published app only — never runs locally) ────

const STORAGE_HOST = 'storage.googleapis.com';
const STORAGE_API = `https://${STORAGE_HOST}/storage/v1`;
const UPLOAD_API = `https://${STORAGE_HOST}/upload/storage/v1`;
const METADATA =
  'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default';

let cachedToken: { value: string; expiresAt: number } | null = null;
let cachedEmail: string | null = null;

async function metadata(suffix: string): Promise<string> {
  const res = await fetch(`${METADATA}/${suffix}`, {
    headers: { 'Metadata-Flavor': 'Google' },
  }).catch(() => null);
  if (!res || !res.ok) {
    throw new Error('File storage is not available right now. Try again in a moment.');
  }
  return res.text();
}

async function accessToken(): Promise<string> {
  if (cachedToken && Date.now() < cachedToken.expiresAt) return cachedToken.value;
  const body = JSON.parse(await metadata('token')) as {
    access_token: string;
    expires_in: number;
  };
  cachedToken = {
    value: body.access_token,
    expiresAt: Date.now() + Math.max(0, body.expires_in - 60) * 1000,
  };
  return cachedToken.value;
}

async function serviceAccountEmail(): Promise<string> {
  if (!cachedEmail) cachedEmail = (await metadata('email')).trim();
  return cachedEmail;
}

/** Authenticated request; drops the cached token and retries once on 401. */
async function apiFetch(
  method: string,
  url: string,
  init: { headers?: Record<string, string>; body?: string | Buffer } = {},
): Promise<Response> {
  for (let attempt = 0; ; attempt++) {
    const res = await fetch(url, {
      method,
      headers: { Authorization: `Bearer ${await accessToken()}`, ...init.headers },
      ...(init.body !== undefined ? { body: init.body as never } : {}),
    });
    if (res.status === 401 && attempt === 0) {
      cachedToken = null;
      continue;
    }
    return res;
  }
}

/** Strict RFC 3986 percent-encoding (unreserved characters only). */
function rfc3986(value: string): string {
  return encodeURIComponent(value).replace(
    /[!'()*]/g,
    (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`,
  );
}

/** Encode each path segment; keep literal "/" separators. */
function encodeObjectPath(object: string): string {
  return object.split('/').map(rfc3986).join('/');
}

// ── The five functions your routes use ─────────────────────────────────────

/** Store a file (creates or replaces). `name` may contain folders: "invoices/2026/07.pdf". */
export async function save(
  name: string,
  data: Buffer | string,
  contentType?: string,
): Promise<void> {
  const body = typeof data === 'string' ? Buffer.from(data, 'utf8') : data;
  const bucket = bucketName();
  if (!bucket) {
    const target = localPath(name);
    fs.mkdirSync(path.dirname(target), { recursive: true });
    fs.writeFileSync(target, body);
    return;
  }
  const object = safeName(name);
  const res = await apiFetch(
    'POST',
    `${UPLOAD_API}/b/${bucket}/o?uploadType=media&name=${encodeURIComponent(object)}`,
    { headers: { 'Content-Type': contentType ?? 'application/octet-stream' }, body },
  );
  if (!res.ok) {
    throw new Error(`Storing "${name}" failed. Try again in a moment.`);
  }
}

/** Read a stored file's full contents. */
export async function read(name: string): Promise<Buffer> {
  const bucket = bucketName();
  if (!bucket) {
    const target = localPath(name);
    if (!fs.existsSync(target)) {
      throw new Error(`There is no stored file named "${name}" yet.`);
    }
    return fs.readFileSync(target);
  }
  const res = await apiFetch(
    'GET',
    `${STORAGE_API}/b/${bucket}/o/${encodeURIComponent(safeName(name))}?alt=media`,
  );
  if (res.status === 404) {
    throw new Error(`There is no stored file named "${name}" yet.`);
  }
  if (!res.ok) {
    throw new Error(`Reading "${name}" failed. Try again in a moment.`);
  }
  return Buffer.from(await res.arrayBuffer());
}

/** List stored file names, optionally under a folder prefix ("invoices/"). */
export async function list(prefix = ''): Promise<string[]> {
  const bucket = bucketName();
  if (!bucket) {
    if (!fs.existsSync(LOCAL_ROOT)) return [];
    const out: string[] = [];
    const walk = (dir: string) => {
      for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
        const full = path.join(dir, entry.name);
        if (entry.isDirectory()) walk(full);
        else out.push(path.relative(LOCAL_ROOT, full).split(path.sep).join('/'));
      }
    };
    walk(LOCAL_ROOT);
    return out.filter((n) => n.startsWith(prefix)).sort();
  }
  const names: string[] = [];
  let pageToken: string | undefined;
  do {
    const params = new URLSearchParams({ fields: 'items(name),nextPageToken' });
    if (prefix) params.set('prefix', prefix);
    if (pageToken) params.set('pageToken', pageToken);
    const res = await apiFetch('GET', `${STORAGE_API}/b/${bucket}/o?${params.toString()}`);
    if (!res.ok) {
      throw new Error('Listing stored files failed. Try again in a moment.');
    }
    const page = (await res.json()) as {
      items?: Array<{ name: string }>;
      nextPageToken?: string;
    };
    for (const item of page.items ?? []) names.push(item.name);
    pageToken = page.nextPageToken;
  } while (pageToken);
  return names.sort();
}

/** Delete a stored file. Deleting a file that doesn't exist is fine. */
export async function remove(name: string): Promise<void> {
  const bucket = bucketName();
  if (!bucket) {
    fs.rmSync(localPath(name), { force: true });
    return;
  }
  const res = await apiFetch(
    'DELETE',
    `${STORAGE_API}/b/${bucket}/o/${encodeURIComponent(safeName(name))}`,
  );
  if (!res.ok && res.status !== 404) {
    throw new Error(`Removing "${name}" failed. Try again in a moment.`);
  }
}

/**
 * A time-limited link anyone can open WITHOUT signing in (default: 60
 * minutes, max 7 days). Use it to let end-users download a stored file.
 * Locally this returns a file:// path so you can eyeball the file; in the
 * published app it's a real expiring web link.
 */
export async function shareLink(name: string, expiresMinutes = 60): Promise<string> {
  const bucket = bucketName();
  if (!bucket) {
    return `file://${localPath(name)}`;
  }
  const object = safeName(name);
  const email = await serviceAccountEmail();

  // Both timestamps derive from ONE UTC instant (no midnight straddle).
  const iso = new Date().toISOString(); // e.g. 2026-07-14T14:25:30.123Z
  const datestamp = iso.slice(0, 10).replace(/-/g, '');
  const timestamp = `${datestamp}T${iso.slice(11, 19).replace(/:/g, '')}Z`;
  const scope = `${datestamp}/auto/storage/goog4_request`;
  const expiresSeconds = Math.min(expiresMinutes, 7 * 24 * 60) * 60; // hard max: 7 days

  const canonicalQuery = (
    [
      ['X-Goog-Algorithm', 'GOOG4-RSA-SHA256'],
      ['X-Goog-Credential', `${email}/${scope}`],
      ['X-Goog-Date', timestamp],
      ['X-Goog-Expires', String(expiresSeconds)],
      ['X-Goog-SignedHeaders', 'host'],
    ] as const
  )
    .map(([k, v]) => `${rfc3986(k)}=${rfc3986(v)}`)
    .sort()
    .join('&');

  const canonicalPath = `/${bucket}/${encodeObjectPath(object)}`;
  const canonicalRequest = [
    'GET',
    canonicalPath,
    canonicalQuery,
    `host:${STORAGE_HOST}`,
    '',
    'host',
    'UNSIGNED-PAYLOAD',
  ].join('\n');

  const stringToSign = [
    'GOOG4-RSA-SHA256',
    timestamp,
    scope,
    crypto.createHash('sha256').update(canonicalRequest, 'utf8').digest('hex'),
  ].join('\n');

  // The app's own platform identity signs the link — no key files anywhere.
  const signRes = await apiFetch(
    'POST',
    `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${email}:signBlob`,
    {
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        payload: Buffer.from(stringToSign, 'utf8').toString('base64'),
      }),
    },
  );
  if (!signRes.ok) {
    throw new Error(`Creating a share link for "${name}" failed. Try again in a moment.`);
  }
  const { signedBlob } = (await signRes.json()) as { signedBlob: string };
  const signature = Buffer.from(signedBlob, 'base64').toString('hex');

  return `https://${STORAGE_HOST}${canonicalPath}?${canonicalQuery}&X-Goog-Signature=${signature}`;
}
