'use strict';

const {
  BODY_LIMIT_LARGE,
  jsonResponse,
  parseLimit,
  readBody,
  send413,
} = require('./api-utils');
const {
  normalizeLocationInput,
  summarizeLocation,
} = require('../location-awareness');
const {
  inferLocationFromRequest,
} = require('../location-inference');

function ensureChatBrain(res, brain, ensureBrainInit) {
  if (!brain || !ensureBrainInit()) {
    jsonResponse(res, { error: 'Brain module not available' }, 503);
    return false;
  }
  return true;
}

const LOCAL_PROVIDER_TYPES = new Set(['ollama', 'mlx']);
const CLOUD_KEY_ENV = {
  anthropic: ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN'],
  openai: ['OPENAI_API_KEY'],
  google: ['GOOGLE_API_KEY', 'GEMINI_API_KEY'],
  deepseek: ['DEEPSEEK_API_KEY'],
  moonshot: ['MOONSHOT_API_KEY'],
};

function providerHasRuntimeAccess(provider) {
  if (!provider || provider.enabled === 0) return false;
  const type = String(provider.type || '').toLowerCase();
  if (LOCAL_PROVIDER_TYPES.has(type)) return true;
  if (provider.api_key_encrypted) return true;
  const envVars = CLOUD_KEY_ENV[type] || [];
  return envVars.some((name) => !!process.env[name]);
}

function modelQualityRank(model) {
  const haystack = `${model.display_name || model.displayName || ''} ${model.model_id || model.modelId || ''}`.toLowerCase();
  if (/sonnet|gpt-5|gemini.*pro|deepseek.*pro|kimi.*k2\.6|kimi.*k2\.5/.test(haystack)) return 0;
  if (/flash|haiku|mini|fast/.test(haystack)) return 1;
  if (/opus|ultra|max/.test(haystack)) return 2;
  return 3;
}

function buildSmartRoutingCandidates(brain, opts = {}) {
  if (!brain || typeof brain.listAllModels !== 'function' || typeof brain.getModelProvider !== 'function') {
    return [];
  }

  const currentProvider = String(opts.currentProvider || '').toLowerCase();
  const currentModel = String(opts.currentModel || '').toLowerCase();
  const providerOrder = [currentProvider, 'openai', 'google', 'moonshot', 'deepseek', 'anthropic', 'ollama', 'mlx']
    .filter(Boolean)
    .filter((value, idx, arr) => arr.indexOf(value) === idx);
  let providerHealth = null;
  try {
    const { default: providerAvailability } = require('../llm/provider-availability');
    providerHealth = new Map(
      providerAvailability.getConfiguredProviders().map((provider) => [provider.providerId, provider]),
    );
  } catch {}

  const candidates = [];
  const rows = brain.listAllModels() || [];
  for (const model of rows) {
    if (!model || model.enabled === 0 || model.enabled === false) continue;
    const provider = brain.getModelProvider(model.provider_id);
    if (!providerHasRuntimeAccess(provider)) continue;
    const providerId = provider.id || model.provider_id;
    if (providerHealth?.get(providerId)?.status === 'unhealthy') continue;
    candidates.push({
      registryId: model.id,
      providerType: String(provider.type || model.provider_type || '').toLowerCase(),
      providerId,
      modelId: model.model_id,
      displayName: model.display_name,
      speedTier: model.speed_tier == null ? 3 : Number(model.speed_tier),
      cost: model.cost_per_1m_input == null ? Number.POSITIVE_INFINITY : Number(model.cost_per_1m_input),
    });
  }

  candidates.sort((a, b) => {
    const ap = providerOrder.indexOf(a.providerType);
    const bp = providerOrder.indexOf(b.providerType);
    const providerCmp = (ap === -1 ? 99 : ap) - (bp === -1 ? 99 : bp);
    if (providerCmp) return providerCmp;
    const qualityCmp = modelQualityRank(a) - modelQualityRank(b);
    if (qualityCmp) return qualityCmp;
    const speedCmp = a.speedTier - b.speedTier;
    if (speedCmp) return speedCmp;
    return a.cost - b.cost;
  });

  const selected = [];
  const seenProviders = new Set();
  for (const candidate of candidates) {
    if (!candidate.providerType || seenProviders.has(candidate.providerType)) continue;
    seenProviders.add(candidate.providerType);
    selected.push(candidate);
  }

  if (opts.includeAutoFirst) {
    const isCurrentModel = (candidate) => {
      if (!currentModel) return false;
      return String(candidate.modelId || '').toLowerCase() === currentModel
        || String(candidate.registryId || '').toLowerCase() === currentModel;
    };
    const sameProviderAlternative = currentProvider && currentModel
      ? candidates.find((candidate) => candidate.providerType === currentProvider && !isCurrentModel(candidate))
      : null;
    const alternatives = selected
      .filter((candidate) => !sameProviderAlternative || candidate.registryId !== sameProviderAlternative.registryId)
      .filter((candidate) => !currentProvider || candidate.providerType !== currentProvider)
      .map((candidate) => candidate.registryId)
      .filter(Boolean);
    const ordered = [
      sameProviderAlternative?.registryId,
      ...alternatives,
    ].filter(Boolean);
    return ordered.length > 0 ? [null, ...ordered.slice(0, 5)] : [];
  }

  return selected.map((candidate) => candidate.registryId).filter(Boolean).slice(0, 6);
}

function buildChatOptionsFromRequestBody(body = {}, {
  req = null,
  parsedAttachments = [],
  abortSignal = null,
} = {}) {
  const context = body.context || undefined;
  const cwd = body.cwd || context?.cwd || context?.projectPath || undefined;
  const chatSessionId = body.chatSessionId || body.chat_session_id || body.session_id || undefined;
  return {
    channel: body.channel || 'ctm',
    session_id: body.session_id || 'default',
    chatSessionId,
    ctmSessionId: body.ctmSessionId || body.ctm_session_id || context?.ctmSessionId || context?.ctm_session_id || undefined,
    agentSessionId: body.agentSessionId || body.agent_session_id || context?.agentSessionId || context?.agent_session_id || chatSessionId,
    agentMode: body.agentMode || body.agent_mode || context?.agentMode || context?.agent_mode || undefined,
    agentKind: body.agentKind || body.agent_kind || context?.agentKind || context?.agent_kind || undefined,
    taskType: body.taskType || body.task_type || context?.taskType || context?.task_type || undefined,
    jsonlPath: body.jsonlPath || body.jsonl_path || context?.jsonlPath || context?.jsonl_path || undefined,
    provider: body.provider || undefined,
    model: body.model || undefined,
    modelPinned: body.modelPinned === true || body.modelExplicit === true,
    allowProviderFallback: body.modelPinned === true || body.modelExplicit === true
      ? false
      : (body.allowProviderFallback === true || body.providerFallback === 'auto'),
    providerFallback: body.providerFallback || undefined,
    thinking: body.thinking || undefined,
    reasoningEffort: body.reasoningEffort || undefined,
    codeReviewMode: body.codeReviewMode || undefined,
    context,
    cwd,
    contextMessages: body.contextMessages || body.context_messages || body.historyOverride || body.history_override || undefined,
    externalActionApprovals: body.externalActionApprovals || body.external_action_approvals || undefined,
    locationRequest: req,
    allowNetworkLocationInference: true,
    abortSignal,
    attachments: parsedAttachments.length ? parsedAttachments : undefined,
  };
}

function handleWalleChatApi(req, res, url, { brain, ensureBrainInit }) {
  const p = url.pathname;
  const m = req.method;
  const params = url.searchParams;

  if (p === '/api/wall-e/location/status' && m === 'GET') {
    if (!ensureChatBrain(res, brain, ensureBrainInit)) return true;
    try {
      const stored = brain.getCurrentLocation({ includeExpired: true });
      const summary = summarizeLocation(stored);
      jsonResponse(res, {
        data: {
          available: !!summary?.fresh,
          location: summary?.fresh ? summary : null,
          stale: summary && !summary.fresh ? summary : null,
        },
      });
    } catch (e) {
      jsonResponse(res, { error: e.message }, 500);
    }
    return true;
  }

  if (p === '/api/wall-e/location' && m === 'POST') {
    if (!ensureChatBrain(res, brain, ensureBrainInit)) return true;
    readBody(req, { maxBytes: 32 * 1024 }).then(body => {
      try {
        const location = normalizeLocationInput(body);
        brain.setCurrentLocation(location);
        jsonResponse(res, { data: { ok: true, location: summarizeLocation(location) } });
      } catch (e) {
        jsonResponse(res, { error: e.message }, 400);
      }
    }).catch(e => jsonResponse(res, { error: e.message }, 400));
    return true;
  }

  if (p === '/api/wall-e/location/infer' && m === 'POST') {
    if (!ensureChatBrain(res, brain, ensureBrainInit)) return true;
    inferLocationFromRequest(req).then(location => {
      brain.setCurrentLocation(location);
      jsonResponse(res, { data: { ok: true, location: summarizeLocation(location) } });
    }).catch(e => jsonResponse(res, { error: e.message }, 424));
    return true;
  }

  if (p === '/api/wall-e/location' && m === 'DELETE') {
    if (!ensureChatBrain(res, brain, ensureBrainInit)) return true;
    try {
      const removed = brain.clearCurrentLocation();
      jsonResponse(res, { data: { ok: true, removed } });
    } catch (e) {
      jsonResponse(res, { error: e.message }, 500);
    }
    return true;
  }

  if (p === '/api/wall-e/chat' && m === 'POST') {
    if (!ensureChatBrain(res, brain, ensureBrainInit)) return true;
    const wantsStream = params.get('stream') === '1' || (req.headers.accept || '').includes('text/event-stream');
    readBody(req, { maxBytes: BODY_LIMIT_LARGE }).then(async body => {
      if (!body.message) {
        jsonResponse(res, { error: 'message is required' }, 400);
        return;
      }
      let requestFinished = false;
      try {
        const abortController = new AbortController();
        if (typeof res.on === 'function') {
          res.on('close', () => {
            if (!requestFinished) abortController.abort();
          });
        }
        const chatModule = require('../chat');
        // Validate + parse attachments before invoking chat. The validator
        // throws AttachmentError with .code + .status so we can surface 4xx
        // upfront instead of silently dropping or 500-ing inside chat.
        // Phase 4 expanded the allowed set to ALL_ALLOWED (image + pdf +
        // audio). Per-attachment capability is still enforced by the
        // resolver below — e.g. audio attachments will 412 if the
        // active provider lacks audio support.
        const { parseAndValidateAttachments, AttachmentError, ALL_ALLOWED, requiredCapabilities } = require('../chat/attachments');
        let parsedAttachments = [];
        try {
          parsedAttachments = parseAndValidateAttachments(body.attachments, { allowed: ALL_ALLOWED });
        } catch (e) {
          if (e instanceof AttachmentError) {
            jsonResponse(res, { error: e.message, code: e.code }, e.status || 400);
            return;
          }
          throw e;
        }

        // Capability resolution: when attachments are present, ensure the
        // active provider can serve them. If not, return 412 with the list
        // of configured alternative providers so the frontend can prompt
        // the user. The user's confirmation re-POSTs with
        // `allowAutoModelSwitch: true` plus the chosen provider/model.
        if (parsedAttachments.length > 0 && !body.allowAutoModelSwitch) {
          try {
            const { resolveCapability } = require('../chat/capability-resolver');
            const { getDefaultProviderType } = require('../llm/client');
            const { default: provAvail } = require('../llm/provider-availability');
            const currentProviderType = body.provider || getDefaultProviderType();
            const needed = requiredCapabilities(parsedAttachments);
            const configured = provAvail.getConfiguredProviders();
            const resolved = resolveCapability(needed, currentProviderType, configured);
            if (!resolved.ok) {
              if (resolved.alternatives.length > 0) {
                jsonResponse(res, {
                  error: `Current provider "${currentProviderType}" lacks required capability(ies): ${resolved.missing.join(', ')}`,
                  code: 'MODEL_CAPABILITY_MISSING',
                  needed: [...needed],
                  missing: resolved.missing,
                  currentProvider: currentProviderType,
                  alternatives: resolved.alternatives,
                }, 412);
                return;
              }
              // No alternatives — caller can't recover; surface 415.
              const providerHint = resolved.missing.includes('audio')
                ? 'OpenAI or Google Gemini'
                : (resolved.missing.includes('pdf')
                  ? 'Anthropic or Google Gemini'
                  : 'Anthropic, OpenAI, Google Gemini, or Moonshot/Kimi');
              jsonResponse(res, {
                error: `No installed provider supports capability(ies): ${resolved.missing.join(', ')}. Install ${providerHint} to use this attachment type.`,
                code: 'NO_CAPABLE_PROVIDER',
                needed: [...needed],
                missing: resolved.missing,
              }, 415);
              return;
            }
          } catch (e) {
            // Resolver bugs must not block real chat traffic — log loudly
            // but fall through to chat.chat which will throw if it can't
            // build content blocks.
            console.error('[chat-api] capability resolver error (non-blocking):', e.message);
          }
        }

        const chatOpts = buildChatOptionsFromRequestBody(body, {
          req,
          parsedAttachments,
          abortSignal: abortController.signal,
        });
        const shouldSmartFallback = !body.model && parsedAttachments.length === 0;
        if (shouldSmartFallback) {
          try {
            const { getDefaultModel, getDefaultProviderType } = require('../llm/client');
            const currentProvider = body.provider
              || brain.getKv?.('walle_provider')
              || process.env.WALLE_PROVIDER
              || getDefaultProviderType();
            const currentModel = body.model
              || brain.getKv?.('walle_model')
              || brain.getKv?.('walle_model_' + currentProvider)
              || process.env.WALLE_MODEL
              || getDefaultModel();
            const candidates = buildSmartRoutingCandidates(brain, {
              currentProvider,
              currentModel,
              includeAutoFirst: true,
            });
            if (candidates.length > 1) {
              chatOpts.model_candidates = candidates;
              chatOpts._reason = 'auto_tier';
            }
          } catch (e) {
            console.error('[chat-api] smart fallback candidate build failed:', e.message);
          }
        }
        if (wantsStream) {
          res.writeHead(200, {
            'Content-Type': 'text/event-stream',
            'Cache-Control': 'no-cache',
            'Connection': 'keep-alive',
          });
          const sendEvent = (event) => {
            try { res.write('data: ' + JSON.stringify(event) + '\n\n'); } catch {}
          };
          const { chatWithFallback } = require('../chat-fallback');
          const call = chatOpts.model_candidates
            ? chatWithFallback(chatModule.chat, body.message, { ...chatOpts, onProgress: sendEvent })
            : chatModule.chat(body.message, { ...chatOpts, onProgress: sendEvent });
          const result = await call;
          requestFinished = true;
          sendEvent({ type: 'done', reply: result.reply, data: result });
          res.end();
        } else {
          try { require('../compat').recordCompatUsage('chat_json_mode'); } catch {}
          const { chatWithFallback } = require('../chat-fallback');
          const result = chatOpts.model_candidates
            ? await chatWithFallback(chatModule.chat, body.message, chatOpts)
            : await chatModule.chat(body.message, chatOpts);
          requestFinished = true;
          jsonResponse(res, { data: result });
        }
      } catch (err) {
        requestFinished = true;
        const isUserCancelled = err?.code === 'WALLE_CANCELLED'
          || err?.message === 'Cancelled'
          || (err?.name === 'AbortError' && abortController.signal.aborted);
        if (isUserCancelled) {
          if (wantsStream) {
            try {
              res.write('data: ' + JSON.stringify({ type: 'cancelled', code: 'WALLE_CANCELLED' }) + '\n\n');
            } catch {}
            try { res.end(); } catch {}
          } else {
            jsonResponse(res, { error: 'Cancelled', code: 'WALLE_CANCELLED' }, 499);
          }
          return;
        }
        try { require('../telemetry').trackError('chat-api', err); } catch {}
        // ALWAYS classify the failure into a clear, human message naming the
        // provider/model — e.g. "AI provider network error: Anthropic (claude-opus-4-8):
        // Wall-E could not reach the provider endpoint…" or "AI model unavailable: …" —
        // instead of leaking a raw, cryptic "fetch failed". classifyProviderError handles
        // any error shape; when the error wasn't decorated deeper in the stack (the path
        // that leaked "fetch failed"), pass the request's provider/model so the message
        // can still name them.
        let providerPayload;
        try {
          const providerErrorMod = require('../llm/provider-error');
          if (err && err.providerError) {
            providerPayload = providerErrorMod.toApiPayload(err); // already carries the resolved provider/model
          } else {
            let defaultProviderType;
            try { defaultProviderType = require('../llm/client').getDefaultProviderType(); } catch {}
            providerPayload = providerErrorMod.toApiPayload(err, {
              provider: body.provider || defaultProviderType || undefined,
              model: body.model || undefined,
            });
          }
        } catch {
          providerPayload = { error: 'AI provider error', message: err.message, code: 'AI_PROVIDER_ERROR' };
        }
        if (wantsStream) {
          try {
            res.write('data: ' + JSON.stringify({
              type: 'error',
              error: providerPayload.message || err.message,
              ...providerPayload,
            }) + '\n\n');
          } catch {}
          try { res.end(); } catch {}
        } else {
          const status = providerPayload?.providerError?.status || err.status || 500;
          jsonResponse(res, providerPayload, status);
        }
      }
    }).catch(e => {
      try { require('../telemetry').trackError('chat-api-outer', e); } catch {}
      if (e.code === 'BODY_TOO_LARGE') {
        if (wantsStream && res.headersSent) {
          try { res.write('data: ' + JSON.stringify({ type: 'error', error: e.message, code: 'BODY_TOO_LARGE' }) + '\n\n'); } catch {}
          try { res.end(); } catch {}
        } else {
          send413(res, e);
        }
        return;
      }
      if (wantsStream) {
        try { res.write('data: ' + JSON.stringify({ type: 'error', error: e.message }) + '\n\n'); } catch {}
        try { res.end(); } catch {}
      } else {
        jsonResponse(res, { error: e.message }, 400);
      }
    });
    return true;
  }

  if (p === '/api/wall-e/chat/history' && m === 'GET') {
    if (!ensureChatBrain(res, brain, ensureBrainInit)) return true;
    try {
      const sessionId = params.get('session_id') || 'default';
      const limit = parseLimit(params, 100);
      const data = brain.listChatMessages({ session_id: sessionId, limit });
      jsonResponse(res, { data });
    } catch (e) {
      jsonResponse(res, { error: e.message }, 500);
    }
    return true;
  }

  if (p === '/api/wall-e/chat/delete' && m === 'POST') {
    if (!ensureChatBrain(res, brain, ensureBrainInit)) return true;
    readBody(req).then(body => {
      try {
        brain.deleteChatMessages({
          session_id: body.session_id || 'default',
          user_content: body.user_content,
          assistant_content: body.assistant_content,
        });
        jsonResponse(res, { data: { ok: true } });
      } catch (e) {
        jsonResponse(res, { error: e.message }, 500);
      }
    }).catch(e => jsonResponse(res, { error: e.message }, 400));
    return true;
  }

  if (p === '/api/wall-e/chat/clear' && m === 'POST') {
    if (!ensureChatBrain(res, brain, ensureBrainInit)) return true;
    readBody(req).then(body => {
      try {
        brain.clearChatSession(body.session_id || 'default');
        jsonResponse(res, { data: { ok: true } });
      } catch (e) {
        jsonResponse(res, { error: e.message }, 500);
      }
    }).catch(e => jsonResponse(res, { error: e.message }, 400));
    return true;
  }

  if (p === '/api/wall-e/chat/insert' && m === 'POST') {
    if (!ensureChatBrain(res, brain, ensureBrainInit)) return true;
    readBody(req).then(body => {
      try {
        brain.insertChatMessage({
          role: body.role,
          content: body.content,
          session_id: body.session_id || 'default',
          channel: body.channel || 'ctm',
        });
        jsonResponse(res, { data: { ok: true } });
      } catch (e) {
        jsonResponse(res, { error: e.message }, 500);
      }
    }).catch(e => jsonResponse(res, { error: e.message }, 400));
    return true;
  }

  if (p === '/api/wall-e/chat/branches' && m === 'GET') {
    if (!ensureChatBrain(res, brain, ensureBrainInit)) return true;
    const sid = params.get('session_id') || 'default';
    try {
      jsonResponse(res, { data: brain.loadChatBranches(sid) });
    } catch (e) {
      jsonResponse(res, { error: e.message }, 500);
    }
    return true;
  }

  if (p === '/api/wall-e/chat/branches' && m === 'POST') {
    if (!ensureChatBrain(res, brain, ensureBrainInit)) return true;
    // A branch snapshot serializes the whole conversation (active history plus
    // every stored branch version), so it routinely outgrows the 1MB default —
    // use the same large limit as the chat-send endpoint, which carries the
    // same message payloads.
    readBody(req, { maxBytes: BODY_LIMIT_LARGE }).then(body => {
      try {
        const updatedAtMs = Number(body.updated_at_ms || body.saved_at_ms || body.updatedAtMs || body.savedAtMs || 0) || Date.now();
        brain.saveChatBranches(body.session_id || 'default', body.branches || {}, body.active || {}, body.history || [], updatedAtMs);
        jsonResponse(res, { data: { ok: true } });
      } catch (e) {
        jsonResponse(res, { error: e.message }, 500);
      }
    }).catch(e => {
      // An oversize body is a 413, not a generic 400, so the client can tell
      // "too big" from a malformed/aborted request.
      if (e && e.code === 'BODY_TOO_LARGE') { send413(res, e); return; }
      jsonResponse(res, { error: e.message }, 400);
    });
    return true;
  }

  if (p === '/api/wall-e/chat/search' && m === 'GET') {
    if (!ensureChatBrain(res, brain, ensureBrainInit)) return true;
    try {
      const q = params.get('q') || '';
      const limit = parseLimit(params, 50);
      const data = brain.searchChatMessages({ query: q, limit });
      jsonResponse(res, { data });
    } catch (e) {
      jsonResponse(res, { error: e.message }, 500);
    }
    return true;
  }

  return false;
}

module.exports = {
  handleWalleChatApi,
  buildChatOptionsFromRequestBody,
  buildSmartRoutingCandidates,
  providerHasRuntimeAccess,
};
