/**
 * Supabase tool adapters.
 *
 * Credential resolution: vault first (getSecret), falling back to process.env
 * for backwards compatibility during the migration window.
 */

import { getSecret } from '../../secrets.js';
import { wrapFetch } from '../../egress.js';

function resolveCredential(name, options = {}) {
  try {
    const secret = getSecret(name, {
      agent_id: options.agent_id,
      project_id: options.project_id,
    });
    if (secret) return secret;
  } catch {
    // Vault not ready — fall through to process.env.
  }
  return process.env[name] || null;
}

function requireCredential(name, options = {}) {
  const value = resolveCredential(name, options);
  if (!value) throw new Error(`${name} not configured`);
  return value;
}

function supabaseAuthHeaders(token) {
  return {
    Authorization: `Bearer ${token}`,
    apikey: token,
    Accept: 'application/json',
  };
}

/**
 * Validate a table name or function name against the safe identifier regex.
 * Allows simple names and schema-qualified names (e.g. auth.users).
 * Rejects anything that could carry SQL injection.
 */
function validateIdentifier(value, label) {
  const name = String(value || '').trim();
  if (!/^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)?$/.test(name)) {
    throw new Error(`${label} must be a valid identifier (got: ${JSON.stringify(value)})`);
  }
  return name;
}

/**
 * Build PostgREST filter query params from a plain object.
 * { column: value } → column=eq.value
 * Values already containing a '.' are passed through as raw PostgREST operators.
 */
function buildFilters(filters) {
  const params = new URLSearchParams();
  for (const [column, value] of Object.entries(filters)) {
    if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(column)) {
      throw new Error(`Invalid filter column: ${column}`);
    }
    params.set(column, String(value).includes('.') ? String(value) : `eq.${value}`);
  }
  return params;
}

/**
 * Extract and return a safe PostgREST error object from a failed response.
 * Never echoes the auth token in the returned object.
 */
async function extractPostgRESTError(res) {
  let body = {};
  try {
    body = await res.json();
  } catch {
    // body is not JSON — use status text only
  }
  return {
    status: res.status,
    message: body.message || body.error || res.statusText || 'Request failed',
    hint: body.hint || null,
  };
}

/**
 * Resolve the Supabase REST base URL and the best available key.
 * Prefers service-role key, falls back to anon key.
 */
function resolveRestCredentials(options) {
  const baseUrl = requireCredential('SUPABASE_URL', options).replace(/\/+$/g, '');
  const key = resolveCredential('SUPABASE_SERVICE_ROLE_KEY', options)
    || resolveCredential('SUPABASE_ANON_KEY', options);
  if (!key) throw new Error('SUPABASE_SERVICE_ROLE_KEY or SUPABASE_ANON_KEY not configured');
  return { baseUrl, key };
}

/**
 * Cap an array of rows at 100 entries.
 */
function capRows(rows) {
  if (!Array.isArray(rows)) return rows;
  return rows.length > 100 ? rows.slice(0, 100) : rows;
}

export async function supabaseListProjects(input = {}, options = {}) {
  const token = requireCredential('SUPABASE_ACCESS_TOKEN', options);
  const agentId = options.agent_id ?? null;
  const res = await wrapFetch('supabase_list_projects', agentId)('https://api.supabase.com/v1/projects', {
    headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' },
  });
  if (!res.ok) throw new Error(`Supabase projects request failed: ${res.status}`);
  const data = await res.json();
  return {
    projects: (Array.isArray(data) ? data : []).map(project => ({
      id: project.id,
      name: project.name,
      region: project.region,
      status: project.status,
      organization_id: project.organization_id,
    })),
  };
}

export async function supabaseRestSelect(input = {}, options = {}) {
  const baseUrl = requireCredential('SUPABASE_URL', options).replace(/\/+$/g, '');
  const key = resolveCredential('SUPABASE_SERVICE_ROLE_KEY', options)
    || resolveCredential('SUPABASE_ANON_KEY', options);
  if (!key) throw new Error('SUPABASE_SERVICE_ROLE_KEY or SUPABASE_ANON_KEY not configured');
  const agentId = options.agent_id ?? null;
  const table = String(input.table || '').trim();
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(table)) throw new Error('table must be a simple table name');
  const limit = Math.min(Math.max(Number(input.limit || 25), 1), 100);
  const url = new URL(`${baseUrl}/rest/v1/${table}`);
  url.searchParams.set('select', String(input.select || '*'));
  url.searchParams.set('limit', String(limit));
  const filters = input.filters && typeof input.filters === 'object' ? input.filters : {};
  for (const [column, value] of Object.entries(filters)) {
    if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(column)) throw new Error(`Invalid filter column: ${column}`);
    url.searchParams.set(column, String(value).includes('.') ? String(value) : `eq.${value}`);
  }
  const res = await wrapFetch('supabase_rest_select', agentId)(url, {
    headers: supabaseAuthHeaders(key),
  });
  if (!res.ok) throw new Error(`Supabase REST select failed: ${res.status}`);
  const rows = await res.json();
  return {
    table,
    count: Array.isArray(rows) ? rows.length : null,
    rows,
  };
}

export async function supabaseRestInsert(input = {}, options = {}) {
  const { baseUrl, key } = resolveRestCredentials(options);
  const agentId = options.agent_id ?? null;
  const table = validateIdentifier(input.table, 'table');

  const rows = Array.isArray(input.rows) ? input.rows : [input.rows].filter(Boolean);
  if (!rows.length) throw new Error('supabase_rest_insert requires at least one row in rows[]');

  const url = new URL(`${baseUrl}/rest/v1/${table}`);
  url.searchParams.set('limit', '100');

  const res = await wrapFetch('supabase_rest_insert', agentId)(url, {
    method: 'POST',
    headers: {
      ...supabaseAuthHeaders(key),
      'Content-Type': 'application/json',
      Prefer: 'return=representation',
    },
    body: JSON.stringify(rows),
  });

  if (!res.ok) {
    const err = await extractPostgRESTError(res);
    throw new Error(`supabase_rest_insert failed (${err.status}): ${err.message}${err.hint ? ` — ${err.hint}` : ''}`);
  }

  const returned = await res.json();
  const cappedRows = capRows(Array.isArray(returned) ? returned : []);
  return { inserted: rows.length, rows: cappedRows };
}

export async function supabaseRestUpsert(input = {}, options = {}) {
  const { baseUrl, key } = resolveRestCredentials(options);
  const agentId = options.agent_id ?? null;
  const table = validateIdentifier(input.table, 'table');

  const rows = Array.isArray(input.rows) ? input.rows : [input.rows].filter(Boolean);
  if (!rows.length) throw new Error('supabase_rest_upsert requires at least one row in rows[]');

  const url = new URL(`${baseUrl}/rest/v1/${table}`);
  url.searchParams.set('limit', '100');
  if (input.on_conflict) {
    url.searchParams.set('on_conflict', String(input.on_conflict));
  }

  const res = await wrapFetch('supabase_rest_upsert', agentId)(url, {
    method: 'POST',
    headers: {
      ...supabaseAuthHeaders(key),
      'Content-Type': 'application/json',
      Prefer: 'resolution=merge-duplicates,return=representation',
    },
    body: JSON.stringify(rows),
  });

  if (!res.ok) {
    const err = await extractPostgRESTError(res);
    throw new Error(`supabase_rest_upsert failed (${err.status}): ${err.message}${err.hint ? ` — ${err.hint}` : ''}`);
  }

  const returned = await res.json();
  const cappedRows = capRows(Array.isArray(returned) ? returned : []);
  return { upserted: rows.length, rows: cappedRows };
}

export async function supabaseRestUpdate(input = {}, options = {}) {
  const { baseUrl, key } = resolveRestCredentials(options);
  const agentId = options.agent_id ?? null;
  const table = validateIdentifier(input.table, 'table');

  // Hard safety constraint: filters are required to avoid table-wide updates.
  const filters = input.filters && typeof input.filters === 'object' ? input.filters : {};
  if (!Object.keys(filters).length) {
    throw new Error('supabase_rest_update requires filters to avoid table-wide updates');
  }

  if (!input.patch || typeof input.patch !== 'object' || Array.isArray(input.patch)) {
    throw new Error('supabase_rest_update requires a patch object');
  }

  const filterParams = buildFilters(filters);
  const url = new URL(`${baseUrl}/rest/v1/${table}?${filterParams.toString()}`);

  const res = await wrapFetch('supabase_rest_update', agentId)(url, {
    method: 'PATCH',
    headers: {
      ...supabaseAuthHeaders(key),
      'Content-Type': 'application/json',
      Prefer: 'return=representation',
      Range: '0-99',
    },
    body: JSON.stringify(input.patch),
  });

  if (!res.ok) {
    const err = await extractPostgRESTError(res);
    throw new Error(`supabase_rest_update failed (${err.status}): ${err.message}${err.hint ? ` — ${err.hint}` : ''}`);
  }

  const returned = await res.json();
  const cappedRows = capRows(Array.isArray(returned) ? returned : []);
  return { updated: cappedRows.length, rows: cappedRows };
}

export async function supabaseRestDelete(input = {}, options = {}) {
  const { baseUrl, key } = resolveRestCredentials(options);
  const agentId = options.agent_id ?? null;
  const table = validateIdentifier(input.table, 'table');

  // Hard constraint: filters are required. Even with approval, a filterless delete is blocked.
  const filters = input.filters && typeof input.filters === 'object' ? input.filters : {};
  if (!Object.keys(filters).length) {
    throw new Error('supabase_rest_delete requires filters; bulk table truncation is blocked');
  }

  const filterParams = buildFilters(filters);
  const url = new URL(`${baseUrl}/rest/v1/${table}?${filterParams.toString()}`);

  const res = await wrapFetch('supabase_rest_delete', agentId)(url, {
    method: 'DELETE',
    headers: {
      ...supabaseAuthHeaders(key),
      Prefer: 'return=representation',
      Range: '0-99',
    },
  });

  if (!res.ok) {
    const err = await extractPostgRESTError(res);
    throw new Error(`supabase_rest_delete failed (${err.status}): ${err.message}${err.hint ? ` — ${err.hint}` : ''}`);
  }

  const returned = await res.json();
  const cappedRows = capRows(Array.isArray(returned) ? returned : []);
  return { deleted: cappedRows.length, rows: cappedRows };
}

export async function supabaseRpc(input = {}, options = {}) {
  const { baseUrl, key } = resolveRestCredentials(options);
  const agentId = options.agent_id ?? null;
  const functionName = validateIdentifier(input.function_name, 'function_name');

  const params = input.params && typeof input.params === 'object' ? input.params : {};

  const url = new URL(`${baseUrl}/rest/v1/rpc/${functionName}`);

  const res = await wrapFetch('supabase_rpc', agentId)(url, {
    method: 'POST',
    headers: {
      ...supabaseAuthHeaders(key),
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(params),
  });

  if (!res.ok) {
    const err = await extractPostgRESTError(res);
    throw new Error(`supabase_rpc(${functionName}) failed (${err.status}): ${err.message}${err.hint ? ` — ${err.hint}` : ''}`);
  }

  const body = await res.text();

  // Cap response to 50KB
  const MAX_BYTES = 50 * 1024;
  if (Buffer.byteLength(body, 'utf8') > MAX_BYTES) {
    throw new Error(`supabase_rpc response too large (>${MAX_BYTES} bytes); narrow your query`);
  }

  let result;
  try {
    result = JSON.parse(body);
  } catch {
    result = body;
  }

  return { result: capRows(Array.isArray(result) ? result : result) };
}

export async function supabaseSql(input = {}, options = {}) {
  const { baseUrl, key } = resolveRestCredentials(options);
  const agentId = options.agent_id ?? null;

  const query = String(input.query || '').trim();
  if (!query) throw new Error('supabase_sql requires a query string');

  const params = input.params && typeof input.params === 'object' ? input.params : {};

  // This adapter calls via the madarx_exec_sql() Postgres function convention.
  // The approval payload (built by the registry gate) must include the RLS-bypass
  // warning and full query text — enforced in the options passed from the registry.
  // Here, for direct calls, we include it in the error context.
  const url = new URL(`${baseUrl}/rest/v1/rpc/madarx_exec_sql`);

  const res = await wrapFetch('supabase_sql', agentId)(url, {
    method: 'POST',
    headers: {
      ...supabaseAuthHeaders(key),
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ query, params }),
  });

  if (res.status === 404) {
    throw new Error(
      'madarx_exec_sql() function not found. ' +
      'Create it in your Supabase project with: ' +
      'CREATE OR REPLACE FUNCTION madarx_exec_sql(query text, params jsonb DEFAULT \'{}\'::jsonb) ' +
      'RETURNS SETOF record LANGUAGE plpgsql SECURITY DEFINER AS $$ BEGIN RETURN QUERY EXECUTE query; END; $$;'
    );
  }

  if (!res.ok) {
    const err = await extractPostgRESTError(res);
    throw new Error(`supabase_sql failed (${err.status}): ${err.message}${err.hint ? ` — ${err.hint}` : ''}`);
  }

  const body = await res.text();

  const MAX_BYTES = 50 * 1024;
  if (Buffer.byteLength(body, 'utf8') > MAX_BYTES) {
    throw new Error(`supabase_sql response too large (>${MAX_BYTES} bytes); add LIMIT to your query`);
  }

  let rows;
  try {
    rows = JSON.parse(body);
  } catch {
    rows = [];
  }

  const cappedRows = capRows(Array.isArray(rows) ? rows : [rows]);
  return { rows: cappedRows, row_count: cappedRows.length };
}
