/**
 * Embeddings configuration shape + helpers.
 *
 * Defaults:
 *   - Embeddings start DISABLED. The user must opt-in via Settings.
 *     This keeps the install footprint smaller (no model downloads,
 *     no API calls) and avoids accidentally embedding sensitive data
 *     before the user has chosen between cloud and local.
 *   - When enabled, local is the recommended default (privacy, $0).
 *
 * Provider config is read from the top-level Loxia config under
 * `embeddings.*`. Per-agent overrides live in the agent's toolConfig
 * (`agent.toolConfig.embeddings.provider`) — handled by the consumer,
 * not this module.
 *
 * Why a module for this: validation + defaults in one place means the
 * UI form, the tests, and the orchestrator all agree on the shape and
 * never drift.
 */

import { DEFAULT_LOCAL_MODEL, LOCAL_MODELS } from './providers/localProvider.js';

/** All known provider names. */
export const PROVIDERS = Object.freeze(['local', 'azure', 'openai', 'azure-custom', 'disabled']);

export const DEFAULT_CONFIG = Object.freeze({
  provider: 'disabled',

  local: Object.freeze({
    modelId: DEFAULT_LOCAL_MODEL,
    // null → use Transformers.js default cache dir; the orchestrator
    // will resolve to `<userDataDir>/state/embeddings/cache/` for us
    // so downloads live alongside other state and survive backups.
    cacheDir: null,
    maxBatchSize: 32,
  }),

  azure: Object.freeze({
    // Loxia backend routes both autopilot + onbuzz to the same shared
    // deployment per current architectural decision. The deployment
    // name lives on the backend side.
    modelId: 'text-embedding-3-small',
    dimensions: 1536,
    maxBatchSize: 16,
  }),

  openai: Object.freeze({
    modelId: 'text-embedding-3-small',
    dimensions: 1536,
    maxBatchSize: 16,
    url: null, // null → official api.openai.com
  }),

  // Azure custom deployment (OpenAI-compat endpoint hosting any embedding
  // model — e.g. Cohere embed-v-4-0, third-party models published to
  // Azure AI Foundry). Speaks the OpenAI REST shape but at the user's
  // own deployment URL. Required: endpoint, deployment, dimensions, key.
  azureCustom: Object.freeze({
    endpoint: '',                // e.g. https://<resource>.services.ai.azure.com/openai/v1/
    deployment: '',              // e.g. "embed-v-4-0"
    dimensions: 1536,            // model-dependent — user supplies
    maxBatchSize: 16,
  }),

  // Per-day soft cap on embedding tokens. Hitting the cap disables
  // cloud embedding for the rest of the day; local is unaffected.
  // Set to 0 / null to disable the cap.
  budget: Object.freeze({
    dailyTokenLimit: 1_000_000,
  }),

  // Auto-recall (silent retrieval injected into the system prompt
  // per-turn). When enabled, the agent's "recent text" is embedded
  // and used to look up high-similarity items from memory + reminisce
  // pools, which are then injected under a clearly-labelled section.
  // Defaults match the literature: threshold ~0.78, topK 3, ~400-token
  // budget — see the Phase 6 design document and the Mem0 / ChatGPT
  // memory reference shapes.
  autoRecall: Object.freeze({
    enabled: false,         // opt-in; the explicit semantic-search action remains the primary path
    threshold: 0.78,        // cosine cutoff — items below this are not injected
    topK: 3,                // max items per turn
    maxTokens: 400,         // hard cap on total recalled token budget per turn
    surfaces: Object.freeze(['memory', 'reminisce']),  // which pools to recall from
  }),
});

/**
 * Deep-merge a user config object into DEFAULT_CONFIG. Frozen defaults
 * stay intact; user overrides win on a per-leaf basis.
 *
 * @param {object|null|undefined} userConfig
 * @returns {object} a fresh, mutable config
 */
export function mergeConfig(userConfig) {
  const out = {
    provider: DEFAULT_CONFIG.provider,
    local: { ...DEFAULT_CONFIG.local },
    azure: { ...DEFAULT_CONFIG.azure },
    openai: { ...DEFAULT_CONFIG.openai },
    azureCustom: { ...DEFAULT_CONFIG.azureCustom },
    budget: { ...DEFAULT_CONFIG.budget },
    autoRecall: { ...DEFAULT_CONFIG.autoRecall, surfaces: [...DEFAULT_CONFIG.autoRecall.surfaces] },
  };
  if (!userConfig || typeof userConfig !== 'object') return out;
  if (typeof userConfig.provider === 'string') out.provider = userConfig.provider;
  for (const key of ['local', 'azure', 'openai', 'azureCustom', 'budget', 'autoRecall']) {
    if (userConfig[key] && typeof userConfig[key] === 'object') {
      out[key] = { ...out[key], ...userConfig[key] };
      // Defensive copy for nested arrays so user mutation doesn't leak
      if (key === 'autoRecall' && Array.isArray(out[key].surfaces)) {
        out[key].surfaces = [...out[key].surfaces];
      }
    }
  }
  return out;
}

/**
 * Lint a config without throwing — returns `{ ok, errors[] }`.
 * Used by the Settings UI to render inline error messages and by the
 * orchestrator to bail loudly on bad config.
 *
 * @param {object} config
 * @returns {{ ok: boolean, errors: string[] }}
 */
export function validateConfig(config) {
  const errors = [];
  if (!config || typeof config !== 'object') {
    return { ok: false, errors: ['config must be an object'] };
  }
  if (!PROVIDERS.includes(config.provider)) {
    errors.push(`provider must be one of ${PROVIDERS.join(', ')}`);
  }
  if (config.provider === 'local') {
    const modelId = config.local?.modelId;
    if (!modelId) errors.push('local.modelId is required');
    // Unknown models are allowed (custom HuggingFace IDs work), but we
    // warn-by-comment in the UI; here we just don't reject them.
    if (config.local?.maxBatchSize != null && (!Number.isInteger(config.local.maxBatchSize) || config.local.maxBatchSize <= 0)) {
      errors.push('local.maxBatchSize must be a positive integer');
    }
  }
  if (config.provider === 'azure') {
    if (!config.azure?.modelId) errors.push('azure.modelId is required');
  }
  if (config.provider === 'openai') {
    if (!config.openai?.modelId) errors.push('openai.modelId is required');
  }
  if (config.provider === 'azure-custom') {
    if (!config.azureCustom?.endpoint) {
      errors.push('azureCustom.endpoint is required (e.g. https://<resource>.services.ai.azure.com/openai/v1/)');
    }
    if (!config.azureCustom?.deployment) {
      errors.push('azureCustom.deployment is required (your deployed model name, e.g. embed-v-4-0)');
    }
    if (config.azureCustom?.dimensions != null) {
      const d = config.azureCustom.dimensions;
      if (!Number.isInteger(d) || d <= 0) {
        errors.push('azureCustom.dimensions must be a positive integer');
      }
    }
  }
  if (config.budget?.dailyTokenLimit != null) {
    const lim = config.budget.dailyTokenLimit;
    if (!(lim === 0 || (Number.isInteger(lim) && lim > 0))) {
      errors.push('budget.dailyTokenLimit must be 0 or a positive integer');
    }
  }
  return { ok: errors.length === 0, errors };
}

/**
 * Surface the list of "known" models the UI can offer to users.
 * Cloud models are static; local models come from the LOCAL_MODELS
 * catalog so adding one there propagates to the UI automatically.
 */
export function getProviderModelOptions(provider) {
  if (provider === 'local') {
    return Object.entries(LOCAL_MODELS).map(([id, info]) => ({
      id,
      description: info.description,
      dimensions: info.dimensions,
    }));
  }
  if (provider === 'azure' || provider === 'openai') {
    return [
      { id: 'text-embedding-3-small', description: 'Best price / quality. Recommended.', dimensions: 1536 },
      { id: 'text-embedding-3-large', description: 'Higher recall, ~5× cost.',           dimensions: 3072 },
    ];
  }
  return [];
}

/**
 * A rough token estimator for telemetry / budget purposes. Real
 * tokenization happens server-side; this is just for the cost-tracking
 * dashboard. ~4 chars/token is the long-standing rule of thumb.
 */
export function estimateTokens(text) {
  if (typeof text !== 'string' || text.length === 0) return 0;
  return Math.ceil(text.length / 4);
}
