import {
  generateKey,
  encrypt,
  decrypt,
  toBase64,
  fromBase64,
  type LedgerData,
  type LedgerMeta,
} from '@splint/shared';

/**
 * API base URL.
 *
 * Priority:
 * 1. VITE_API_BASE env var (set at build time for custom API paths)
 * 2. Vite's BASE_URL (from VITE_BASE config — e.g. /splint/ in prod)
 * 3. Fallback: http://localhost:3000 (standalone dev, no subpath)
 *
 * Trailing slash is stripped to avoid double-slash bugs in template literals.
 * With BASE_URL='/', `${API_BASE}/ledgers` would produce `//ledgers` (a
 * protocol-relative URL → https://ledgers/) instead of `/ledgers`. Stripping
 * the trailing slash prevents this: '/' → '' → '/ledgers' works correctly.
 *
 * In production with VITE_BASE=/splint/:
 *   import.meta.env.BASE_URL = '/splint/'
 *   API_BASE = '/splint' (trailing slash stripped)
 *   → `${API_BASE}/ledgers` = '/splint/ledgers'
 *
 * In production with default base:
 *   import.meta.env.BASE_URL = '/'
 *   API_BASE = '' (trailing slash stripped)
 *   → `${API_BASE}/ledgers` = '/ledgers'
 *
 * In dev (default):
 *   API_BASE = 'http://localhost:3000' (no trailing slash, unchanged)
 *   → `${API_BASE}/ledgers` = 'http://localhost:3000/ledgers'
 */
const API_BASE = (import.meta.env.VITE_API_BASE || import.meta.env.BASE_URL || 'http://localhost:3000').replace(/\/$/, '');

// ── Error Classification ────────────────────────────────────────────────────

export enum ErrorKind {
  Network = 'network',
  Server = 'server',
  Decryption = 'decryption',
  Unknown = 'unknown',
}

export class SplintError extends Error {
  kind: ErrorKind;
  status?: number;

  constructor(kind: ErrorKind, message: string, status?: number) {
    super(message);
    this.name = 'SplintError';
    this.kind = kind;
    this.status = status;
  }
}

function classifyError(err: unknown, operation: string): SplintError {
  if (err instanceof SplintError) return err;

  // Network errors: fetch failed (offline, DNS, connection refused)
  if (err instanceof TypeError && err.message === 'Failed to fetch') {
    return new SplintError(
      ErrorKind.Network,
      `Server unreachable — check your connection and try again.`,
    );
  }

  const message = err instanceof Error ? err.message : 'Unknown error';

  // Decryption / key errors
  if (
    message.includes('decrypt') ||
    message.includes('Decryption') ||
    message.includes('ciphertext') ||
    message.includes('bad key') ||
    message.includes('Wrong key')
  ) {
    return new SplintError(
      ErrorKind.Decryption,
      `Wrong key — this link may be invalid or the ledger key has changed.`,
    );
  }

  return new SplintError(ErrorKind.Unknown, `Failed to ${operation}: ${message}`);
}

// ── Key Serialization Helpers ────────────────────────────────────────────────

async function exportKeyToBase64(key: CryptoKey): Promise<string> {
  const raw = await crypto.subtle.exportKey('raw', key);
  return toBase64(new Uint8Array(raw));
}

async function importKeyFromBase64(keyBase64: string): Promise<CryptoKey> {
  const raw = fromBase64(keyBase64);
  return crypto.subtle.importKey(
    'raw',
    raw.buffer as ArrayBuffer,
    { name: 'AES-GCM', length: 256 },
    true,
    ['encrypt', 'decrypt'],
  );
}

// ── Data Encryption Helpers ──────────────────────────────────────────────────

async function encryptData(key: CryptoKey, data: LedgerData): Promise<string> {
  const json = JSON.stringify(data);
  const { ciphertext, iv } = await encrypt(key, json);
  const payload = JSON.stringify({ ciphertext, iv });
  return toBase64(new TextEncoder().encode(payload));
}

async function decryptData(key: CryptoKey, encryptedData: string): Promise<LedgerData> {
  const payloadBytes = fromBase64(encryptedData);
  const payloadJson = new TextDecoder().decode(payloadBytes);
  const { ciphertext, iv }: { ciphertext: string; iv: string } = JSON.parse(payloadJson);
  const plaintext = await decrypt(key, ciphertext, iv);
  return JSON.parse(plaintext) as LedgerData;
}

// ── Public API ───────────────────────────────────────────────────────────────

export async function createLedger(
  name: string,
  initialData: LedgerData,
): Promise<{ id: string; key: string; meta: LedgerMeta }> {
  try {
    const id = crypto.randomUUID();
    const key = await generateKey();
    const keyBase64 = await exportKeyToBase64(key);
    const encryptedData = await encryptData(key, initialData);

    const response = await fetch(`${API_BASE}/ledgers`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ id, name, encryptedData }),
    });

    if (!response.ok) {
      throw new SplintError(
        ErrorKind.Server,
        `Failed to create ledger: server returned ${response.status}`,
        response.status,
      );
    }

    const { id: serverId } = await response.json() as { id: string; name: string };
    const now = new Date().toISOString();
    const meta: LedgerMeta = { id: serverId, name, createdAt: now, updatedAt: now };

    return { id: serverId, key: keyBase64, meta };
  } catch (err) {
    throw classifyError(err, 'create ledger');
  }
}

export async function getLedger(
  id: string,
  keyBase64: string,
): Promise<{ meta: LedgerMeta; data: LedgerData }> {
  try {
    const key = await importKeyFromBase64(keyBase64);

    const response = await fetch(`${API_BASE}/ledgers/${id}`);

    if (response.status === 404) {
      throw new SplintError(
        ErrorKind.Server,
        'Ledger not found — check that the link and your encryption key are correct.',
        404,
      );
    }

    if (!response.ok) {
      throw new SplintError(
        ErrorKind.Server,
        `Failed to load ledger: server returned ${response.status}`,
        response.status,
      );
    }

    const json: {
      name: string;
      encryptedData: string;
      createdAt: string;
      updatedAt: string;
    } = await response.json();

    let data: LedgerData;
    try {
      data = await decryptData(key, json.encryptedData);
    } catch {
      throw new SplintError(
        ErrorKind.Decryption,
        'Wrong key — this link may be invalid or the ledger key has changed.',
      );
    }

    const meta: LedgerMeta = {
      id,
      name: json.name,
      createdAt: json.createdAt,
      updatedAt: json.updatedAt,
    };

    return { meta, data };
  } catch (err) {
    throw classifyError(err, 'load ledger');
  }
}

export async function deleteLedger(id: string): Promise<void> {
  try {
    const response = await fetch(`${API_BASE}/ledgers/${id}`, {
      method: 'DELETE',
    });

    if (response.status === 404) {
      throw new SplintError(
        ErrorKind.Server,
        'Ledger not found — it may have already been deleted.',
        404,
      );
    }

    if (!response.ok) {
      throw new SplintError(
        ErrorKind.Server,
        `Failed to delete ledger: server returned ${response.status}`,
        response.status,
      );
    }
  } catch (err) {
    throw classifyError(err, 'delete ledger');
  }
}

export async function updateLedger(
  id: string,
  keyBase64: string,
  data: LedgerData,
): Promise<void> {
  try {
    const key = await importKeyFromBase64(keyBase64);
    const encryptedData = await encryptData(key, data);

    const response = await fetch(`${API_BASE}/ledgers/${id}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ encryptedData }),
    });

    if (!response.ok) {
      throw new SplintError(
        ErrorKind.Server,
        `Failed to save changes: server returned ${response.status}`,
        response.status,
      );
    }
  } catch (err) {
    throw classifyError(err, 'save changes');
  }
}
