/**
 * Client-side sync of a job's deployable files into the Provider Vault
 * granted folder (File System Access via the extension).
 *
 * Transport:
 *  - Hosted (rha.software): chrome.runtime.sendMessage(extensionId, …)
 *  - Localhost: window.postMessage → content script → background
 */

const CHUNK_BYTES = 180_000; // keep base64 payloads under ~240KB
const SOURCE = 'rha-web';

export type DeploySyncFileResult = {
  path: string;
  ok: boolean;
  skipped?: boolean;
  error?: string;
  bytes?: number;
};

export type DeploySyncResult = {
  ok: boolean;
  folderName: string | null;
  written: number;
  skipped: number;
  failed: number;
  files: DeploySyncFileResult[];
  error?: string;
};

type ExtBridge = { extensionId?: string; version?: string };

function readExt(): ExtBridge {
  const w = window as Window & { __RHA_EXT__?: ExtBridge };
  if (w.__RHA_EXT__?.extensionId) return w.__RHA_EXT__;
  try {
    const raw = document.documentElement.dataset.rhaExt;
    if (raw) return JSON.parse(raw) as ExtBridge;
  } catch { /* ignore */ }
  return {};
}

function getChromeRuntime(): {
  sendMessage: (
    extensionId: string,
    message: unknown,
    responseCallback?: (response: Record<string, unknown>) => void,
  ) => void;
  lastError?: { message: string };
} | null {
  const c = (globalThis as { chrome?: { runtime?: {
    sendMessage: (
      extensionId: string,
      message: unknown,
      responseCallback?: (response: Record<string, unknown>) => void,
    ) => void;
    lastError?: { message: string };
  } } }).chrome;
  return c?.runtime?.sendMessage ? c.runtime : null;
}

function extSend(payload: Record<string, unknown>): Promise<Record<string, unknown>> {
  const { extensionId } = readExt();
  const runtime = getChromeRuntime();

  if (runtime && extensionId) {
    return new Promise((resolve, reject) => {
      try {
        runtime.sendMessage(extensionId, payload, (response: Record<string, unknown>) => {
          const err = runtime.lastError;
          if (err) reject(new Error(err.message));
          else resolve(response ?? {});
        });
      } catch (e) {
        reject(e instanceof Error ? e : new Error(String(e)));
      }
    });
  }

  // Localhost / content-script relay
  const id = `deploy-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => {
      window.removeEventListener('message', onMsg);
      reject(new Error('extension deploy bridge timed out — is Provider Vault loaded?'));
    }, 60_000);
    const onMsg = (ev: MessageEvent) => {
      const d = ev.data as { type?: string; id?: string; response?: Record<string, unknown> } | null;
      if (!d || d.type !== 'rha-deploy-res' || d.id !== id) return;
      clearTimeout(timer);
      window.removeEventListener('message', onMsg);
      resolve(d.response ?? {});
    };
    window.addEventListener('message', onMsg);
    window.postMessage({ type: 'rha-deploy-req', source: SOURCE, id, payload }, '*');
  });
}

function bytesToBase64(bytes: Uint8Array): string {
  let bin = '';
  const chunk = 0x8000;
  for (let i = 0; i < bytes.length; i += chunk) {
    bin += String.fromCharCode(...bytes.subarray(i, i + chunk));
  }
  return btoa(bin);
}

function syncKey(jobId: string, folderName: string): string {
  return `rha-deploy-hashes:${jobId}:${folderName}`;
}

function loadPrevHashes(jobId: string, folderName: string): Record<string, string> {
  try {
    const raw = localStorage.getItem(syncKey(jobId, folderName));
    return raw ? JSON.parse(raw) as Record<string, string> : {};
  } catch {
    return {};
  }
}

function saveHashes(jobId: string, folderName: string, hashes: Record<string, string>): void {
  localStorage.setItem(syncKey(jobId, folderName), JSON.stringify(hashes));
}

async function writeFileViaExt(
  path: string,
  bytes: Uint8Array,
  sha256: string,
): Promise<{ ok: boolean; error?: string; bytes?: number }> {
  if (bytes.length <= CHUNK_BYTES) {
    const res = await extSend({
      action: 'deploy:write-file',
      path,
      sha256,
      dataBase64: bytesToBase64(bytes),
    });
    if (res.error || res.ok === false) {
      return { ok: false, error: String(res.error ?? 'write failed') };
    }
    return { ok: true, bytes: bytes.length };
  }

  const totalChunks = Math.ceil(bytes.length / CHUNK_BYTES);
  const begin = await extSend({
    action: 'deploy:write-begin',
    path,
    sha256,
    totalChunks,
    totalBytes: bytes.length,
  });
  if (begin.error || begin.ok === false) {
    return { ok: false, error: String(begin.error ?? 'write-begin failed') };
  }
  for (let i = 0; i < totalChunks; i++) {
    const slice = bytes.subarray(i * CHUNK_BYTES, (i + 1) * CHUNK_BYTES);
    const chunkRes = await extSend({
      action: 'deploy:write-chunk',
      path,
      index: i,
      dataBase64: bytesToBase64(slice),
    });
    if (chunkRes.error || chunkRes.ok === false) {
      return { ok: false, error: String(chunkRes.error ?? `chunk ${i} failed`) };
    }
  }
  const end = await extSend({ action: 'deploy:write-end', path, sha256 });
  if (end.error || end.ok === false) {
    return { ok: false, error: String(end.error ?? 'write-end failed') };
  }
  return { ok: true, bytes: bytes.length };
}

export async function getDeployFolderStatus(): Promise<{
  hasHandle: boolean;
  folderName: string | null;
  granted: boolean;
  error?: string;
}> {
  try {
    const res = await extSend({ action: 'deploy:status' });
    if (res.error) return { hasHandle: false, folderName: null, granted: false, error: String(res.error) };
    return {
      hasHandle: !!res.hasHandle,
      folderName: (res.folderName as string) ?? null,
      granted: !!res.granted,
    };
  } catch (e) {
    return {
      hasHandle: false,
      folderName: null,
      granted: false,
      error: e instanceof Error ? e.message : String(e),
    };
  }
}

/**
 * Pull deployable files from the job API and push changed ones to the extension folder.
 */
export async function syncJobToDeployFolder(
  jobId: string,
  opts: { ledger?: boolean; onFile?: (r: DeploySyncFileResult) => void } = {},
): Promise<DeploySyncResult> {
  const ensure = await extSend({ action: 'deploy:ensure' });
  if (ensure.error || ensure.ok === false) {
    return {
      ok: false,
      folderName: null,
      written: 0,
      skipped: 0,
      failed: 0,
      files: [],
      error: String(ensure.error ?? 'deploy folder not ready — grant a folder in Provider Vault options'),
    };
  }
  const folderName = String(ensure.folderName ?? 'deploy');

  const q = opts.ledger ? '?ledger=1' : '';
  const manifest = await fetch(`/api/jobs/${jobId}/deploy-manifest${q}`).then(async r => {
    if (!r.ok) throw new Error(await r.text());
    return r.json() as Promise<{
      files: { path: string; sha256: string; size: number }[];
    }>;
  });

  const prev = loadPrevHashes(jobId, folderName);
  const nextHashes: Record<string, string> = { ...prev };
  const files: DeploySyncFileResult[] = [];
  let written = 0;
  let skipped = 0;
  let failed = 0;

  for (const entry of manifest.files) {
    if (prev[entry.path] === entry.sha256) {
      const r: DeploySyncFileResult = { path: entry.path, ok: true, skipped: true };
      files.push(r);
      opts.onFile?.(r);
      skipped++;
      nextHashes[entry.path] = entry.sha256;
      continue;
    }
    try {
      const res = await fetch(
        `/api/jobs/${jobId}/deploy-file?path=${encodeURIComponent(entry.path)}`,
      );
      if (!res.ok) throw new Error(await res.text());
      const buf = new Uint8Array(await res.arrayBuffer());
      const write = await writeFileViaExt(entry.path, buf, entry.sha256);
      if (!write.ok) throw new Error(write.error ?? 'write failed');
      const r: DeploySyncFileResult = { path: entry.path, ok: true, bytes: write.bytes };
      files.push(r);
      opts.onFile?.(r);
      written++;
      nextHashes[entry.path] = entry.sha256;
    } catch (e) {
      const r: DeploySyncFileResult = {
        path: entry.path,
        ok: false,
        error: e instanceof Error ? e.message : String(e),
      };
      files.push(r);
      opts.onFile?.(r);
      failed++;
    }
  }

  if (failed === 0) saveHashes(jobId, folderName, nextHashes);

  return {
    ok: failed === 0,
    folderName,
    written,
    skipped,
    failed,
    files,
    error: failed ? `${failed} file(s) failed to sync` : undefined,
  };
}
