'use strict';

const crypto = require('crypto');

function normalizeText(value) {
  return String(value || '').replace(/\s+/g, ' ').trim();
}

function includesAny(text, patterns) {
  return patterns.some((pattern) => pattern.test(text));
}

const CHANGE_PATTERNS = [
  /\bfix(?:es|ed|ing)?\b/i,
  /\bimplement(?:s|ed|ing)?\b/i,
  /\bchange(?:s|d|ing)?\b/i,
  /\bupdate(?:s|d|ing)?\b/i,
  /\bedit(?:s|ed|ing)?\b/i,
  /\bmodify(?:ies|ied|ing)?\b/i,
  /\bimprove(?:s|d|ing)?\b/i,
  /\bpolish(?:es|ed|ing)?\b/i,
  /\benhance(?:s|d|ing)?\b/i,
  /\bupgrade(?:s|d|ing)?\b/i,
  /\brefactor(?:s|ed|ing)?\b/i,
  /\bwire(?:s|d|ing)?\b/i,
  /\badd(?:s|ed|ing)?\b/i,
  /\bremove(?:s|d|ing)?\b/i,
  /\bdelete(?:s|d|ing)?\b/i,
  /\bbuild(?:s|ing)?\b/i,
  /\bcreate(?:s|d|ing)?\b/i,
  /\bgenerate(?:s|d|ing)?\b/i,
  /\bwrite(?:s|ing|ten)?\b/i,
  /\bdocument(?:s|ed|ing)?\b/i,
  /\btest(?:s|ed|ing)?\b/i,
  /\bcommit(?:s|ted|ting)?\b/i,
  /\bmerge(?:s|d|ing)?\b/i,
];

const READ_ONLY_PATTERNS = [
  /\b(?:don'?t|do not) (?:do|make|change|implement|edit|fix|touch)\b/i,
  /\bno (?:code )?(?:change|changes|edits|implementation)\b/i,
  /\bjust (?:answer|respond|reply|explain|discuss)\b/i,
  /\bdiscuss (?:first|before)\b/i,
  /\blet'?s discuss\b/i,
  /\bproposal\b/i,
  /\bdesign\b/i,
  /\bplan\b/i,
  /\bwhy\b/i,
  /\bexplain\b/i,
  /\banaly[sz]e\b/i,
  /\binvestigate\b/i,
  /\bstudy\b/i,
  /\breview\b/i,
];

const CONVERSATIONAL_UPDATE_PATTERNS = [
  /\b(?:update|refine|revise|correct|expand|clarify|adjust|improve)\s+(?:your|the|that|this|my|above|previous|prior|last)?\s*(?:answer|summary|analysis|response|reply|conclusion|search|result|results|finding|findings|write[- ]?up)\b/i,
  /\b(?:refine|broaden|narrow|redo|rerun)\s+(?:your|the|that|this|my|above|previous|prior|last)?\s*search\b/i,
  /\b(?:update|refine|revise|correct|expand|clarify|adjust|improve)\s+(?:it|that|this)\s+(?:above|based on|with|using)\b/i,
];

const WORKSPACE_MUTATION_TARGET_PATTERNS = [
  /\b(?:file|files|code|repo|repository|worktree|project|app|source|implementation|docs?|documentation|readme|markdown|test|tests|spec|specs|db|database|schema|server|client|frontend|backend|component|module|package|script|config)\b/i,
  /\b(?:in|to|under)\s+[`'"]?[^`'"\s]+\.(?:js|mjs|cjs|ts|tsx|jsx|md|mdx|json|py|go|rs|swift|html|css|scss|sql|yml|yaml|toml|sh|rb|java|kt|xml)\b/i,
  /(?:^|[\s"'`])(?:\.{0,2}\/|~\/)[^\s"'`]+/,
  /\b[\w.-]+\.(?:js|mjs|cjs|ts|tsx|jsx|md|mdx|json|py|go|rs|swift|html|css|scss|sql|yml|yaml|toml|sh|rb|java|kt|xml)\b/i,
];

function classifyCodingTurnIntent(prompt) {
  const text = normalizeText(prompt);
  const asksChange = includesAny(text, CHANGE_PATTERNS);
  const asksReadOnly = includesAny(text, READ_ONLY_PATTERNS);
  const asksConversationalUpdate = includesAny(text, CONVERSATIONAL_UPDATE_PATTERNS);
  const hasWorkspaceMutationTarget = includesAny(text, WORKSPACE_MUTATION_TARGET_PATTERNS);
  const explicitNoChange = /\b(?:don'?t|do not) (?:do|make|change|implement|edit|fix|touch)\b/i.test(text)
    || /\bno (?:code )?(?:change|changes|edits|implementation)\b/i.test(text);
  const conversationalUpdateOnly = asksConversationalUpdate && !hasWorkspaceMutationTarget;
  const effectiveAsksChange = asksChange && !conversationalUpdateOnly;
  const readOnly = explicitNoChange || conversationalUpdateOnly || (asksReadOnly && !effectiveAsksChange);
  return {
    kind: effectiveAsksChange && !explicitNoChange ? 'coding_change' : (readOnly ? 'read_only' : 'coding_unknown'),
    expectsChange: effectiveAsksChange && !explicitNoChange,
    readOnly,
    explicitNoChange,
    reason: effectiveAsksChange && !explicitNoChange
      ? 'change_language'
      : (conversationalUpdateOnly
        ? 'conversational_update_language'
        : (readOnly ? 'read_only_language' : 'ambiguous_coding_turn')),
  };
}

function collectToolCalls(result = {}) {
  const calls = [];
  for (const turn of result.log || []) {
    for (const call of turn.toolCalls || []) calls.push(call);
  }
  return calls;
}

function collectToolResults(result = {}) {
  const results = [];
  for (const turn of result.log || []) {
    for (const item of turn.toolResults || []) results.push(item);
  }
  return results;
}

function toolName(value) {
  return String(value?.name || value?.tool || value?.toolName || value?.tool_name || '').toLowerCase();
}

function commandText(call = {}) {
  const input = call.input || call.args || {};
  return String(input.command || input.cmd || input.script || '');
}

function hasEditTool(result = {}) {
  return collectToolCalls(result).some((call) => /^(edit_file|write_file|apply_patch|multi_edit)$/.test(toolName(call)));
}

function hasVerificationTool(result = {}) {
  const calls = collectToolCalls(result);
  const results = collectToolResults(result);
  return calls.some((call, index) => {
    const name = toolName(call);
    const cmd = commandText(call);
    const looksLikeVerification = /^(run_shell|shell|exec|bash|terminal)$/.test(name)
      && /\b(test|lint|check|typecheck|tsc|pytest|vitest|jest|playwright|node --test|npm test|npm run test)\b/i.test(cmd);
    if (!looksLikeVerification) return false;
    const paired = results[index] || {};
    return paired.ok === true || paired.result?.ok === true || paired.result?.exitCode === 0 || paired.result?.exit_code === 0;
  });
}

function normalizeChangedFiles(files) {
  if (!Array.isArray(files)) return [];
  return [...new Set(files.map((file) => normalizeText(file)).filter(Boolean))].slice(0, 200);
}

function makeBlockedReply({ output, reason }) {
  const tail = normalizeText(output).slice(0, 1800);
  return [
    'I did not make code changes, so I am not marking this coding task complete.',
    '',
    `Blocked reason: ${reason}.`,
    tail ? `\nLast agent output:\n\n${tail}` : '',
  ].join('\n').trim();
}

function hasDeliveredFinalAnswer(result = {}) {
  if (result.finalAnswerDelivered === true) return true;
  if (result.finalAnswerDelivered === false) return false;
  if (normalizeText(result.output || result.reply || '').length > 0) return true;
  const finalTurn = [...(result.log || [])].reverse().find((turn) => Array.isArray(turn.toolCalls) && turn.toolCalls.length === 0);
  return normalizeText(finalTurn?.content || '').length > 0;
}

function makeIncompleteReadOnlyReply({ output, stderr } = {}) {
  const detail = normalizeText(stderr || output || '').slice(0, 1000);
  return [
    'The agent did not produce a final answer for this follow-up.',
    detail ? `\nRuntime detail: ${detail}` : '',
  ].join('\n').trim();
}

function deriveCodingOutcome(result = {}, { prompt = '', interactive = false } = {}) {
  const intent = classifyCodingTurnIntent(prompt);
  const changedFiles = normalizeChangedFiles(result.changedFiles);
  const edited = changedFiles.length > 0 || hasEditTool(result);
  const verified = hasVerificationTool(result);
  const baseSuccess = result.success !== false && Number(result.exitCode || 0) === 0;
  if (!baseSuccess) {
    return {
      status: 'failed',
      success: false,
      reason: 'agent_runtime_failed',
      changedFiles,
      verified,
      intent,
    };
  }
  // Interactive (opencode parity): never block/fail on "no file changes". The turn is
  // successful as long as the agent delivered a final answer. Reuses existing success
  // statuses (`read_only_answer`, `completed_with_changes`) so no new outcome.status enum
  // is introduced — interactive simply never emits `blocked`.
  if (interactive) {
    if (!hasDeliveredFinalAnswer(result)) {
      return {
        status: 'failed',
        success: false,
        reason: 'agent_finished_without_answer',
        changedFiles,
        verified,
        intent,
      };
    }
    if (changedFiles.length > 0) {
      return {
        status: 'completed_with_changes',
        success: true,
        reason: verified ? 'changed_and_verified' : 'changed_unverified',
        changedFiles,
        verified,
        intent,
      };
    }
    return {
      status: 'read_only_answer',
      success: true,
      reason: 'answer_delivered',
      changedFiles,
      verified,
      intent,
    };
  }
  if (intent.expectsChange && changedFiles.length === 0) {
    return {
      status: 'blocked',
      success: false,
      reason: edited ? 'edit_tool_produced_no_diff' : 'coding_task_finished_without_changes',
      changedFiles,
      verified,
      intent,
    };
  }
  if (intent.readOnly && changedFiles.length === 0) {
    if (!hasDeliveredFinalAnswer(result)) {
      return {
        status: 'failed',
        success: false,
        reason: 'agent_finished_without_answer',
        changedFiles,
        verified,
        intent,
      };
    }
    return {
      status: 'read_only_answer',
      success: true,
      reason: intent.reason,
      changedFiles,
      verified,
      intent,
    };
  }
  if (changedFiles.length > 0) {
    return {
      status: 'completed_with_changes',
      success: true,
      reason: verified ? 'changed_and_verified' : 'changed_unverified',
      changedFiles,
      verified,
      intent,
    };
  }
  return {
    status: 'completed_no_changes',
    success: true,
    reason: 'no_change_required_or_detected',
    changedFiles,
    verified,
    intent,
  };
}

function resolveCodingProvider({ model, provider, providerFactory } = {}) {
  if (typeof providerFactory === 'function') {
    return {
      providerClient: providerFactory(provider || null, {}),
      model: model || null,
      route: null,
    };
  }
  if (!model && !provider && typeof providerFactory !== 'function') return { providerClient: null, model: model || null, route: null };
  let route = null;
  let resolvedProvider = provider || null;
  let resolvedModel = model || null;
  let providerConfig = {};
  if (model) {
    const { resolveModelSelection } = require('../chat');
    route = resolveModelSelection(model, provider || undefined);
    if (route?.ambiguous) {
      const choices = (route.candidates || []).map((candidate) => `${candidate.registryId} (${candidate.label})`).join(', ');
      throw new Error(`Ambiguous model route for "${route.input}". Choose a specific model route: ${choices}`);
    }
    resolvedProvider = route.provider || resolvedProvider;
    resolvedModel = route.model || resolvedModel;
    providerConfig = route.providerConfig || {};
  }
  if (!resolvedProvider && !providerFactory) return { providerClient: null, model: resolvedModel, route };
  const { createClient, getProviderRuntimeConfig } = require('../llm/client');
  const providerClient = createClient(resolvedProvider, getProviderRuntimeConfig(resolvedProvider, providerConfig));
  return { providerClient, model: resolvedModel, route };
}

class CodingRunController {
  constructor({ runAgentLoop = null, now = () => Date.now(), idFactory = () => crypto.randomUUID() } = {}) {
    this.runAgentLoop = runAgentLoop || require('../coding-orchestrator').runAgentLoop;
    this.now = now;
    this.idFactory = idFactory;
  }

  async runTurn(request = {}, { onEvent = null, signal = null } = {}) {
    const prompt = normalizeText(request.message || request.request || request.prompt || '');
    if (!prompt) throw new Error('message is required');
    const codingRunId = normalizeText(request.codingRunId || request.coding_run_id || request.agentSessionId || request.agent_session_id || request.session_id)
      || `coding-${this.idFactory()}`;
    const cwd = request.cwd || request.context?.cwd || request.context?.projectPath || process.cwd();
    const startedAt = this.now();
    const progress = (event) => {
      if (typeof onEvent === 'function') onEvent(event);
    };
    const providerRoute = resolveCodingProvider({
      model: request.model,
      provider: request.provider,
      providerFactory: request.providerFactory,
    });
    const codingIntent = classifyCodingTurnIntent(prompt);
    progress({
      type: 'coding_run_started',
      codingRunId,
      sessionId: request.session_id || request.chatSessionId || '',
      ctmSessionId: request.ctmSessionId || request.ctm_session_id || '',
      cwd,
      model: providerRoute.model || request.model || '',
      provider: providerRoute.providerClient?.type || request.provider || providerRoute.route?.provider || '',
      intent: codingIntent,
    });
    const result = await this.runAgentLoop(prompt, {
      cwd,
      model: providerRoute.model || request.model,
      provider: providerRoute.providerClient || undefined,
      codingIntent,
      timeoutMs: request.timeoutMs || request.timeout_ms,
      maxTurns: request.maxTurns || request.max_turns,
      mode: request.mode || 'build',
      channel: request.channel || 'ctm-session',
      agentMode: request.agentMode || request.agent_mode || 'coding',
      agentKind: request.agentKind || request.agent_kind || 'walle-coding',
      taskType: request.taskType || request.task_type || 'coding',
      session_id: request.session_id || request.chatSessionId || codingRunId,
      ctmSessionId: request.ctmSessionId || request.ctm_session_id || '',
      agentSessionId: request.agentSessionId || request.agent_session_id || codingRunId,
      chatSessionId: request.chatSessionId || request.chat_session_id || request.session_id || '',
      persistTranscript: request.persistTranscript,
      transcript: request.transcript,
      contextMessages: request.contextMessages || request.context_messages,
      transcriptMessageOwner: request.transcriptMessageOwner || request.transcript_message_owner,
      externalTranscriptMessages: request.externalTranscriptMessages ?? request.external_transcript_messages,
      skipTranscriptMessages: request.skipTranscriptMessages ?? request.skip_transcript_messages,
      promptCapabilities: request.promptCapabilities,
      promptCapabilityHints: request.promptCapabilityHints,
      permissionService: request.permissionService,
      headlessPolicy: request.headlessPolicy,
      benchmark: request.benchmark,
      headless: request.headless,
      interactive: request.interactive,
      abortSignal: signal,
      onProgress: progress,
      runSessionId: codingRunId,
    });
    const { isInteractiveRun } = require('../coding-orchestrator');
    const interactive = isInteractiveRun(request);
    const outcome = deriveCodingOutcome(result, { prompt, interactive });
    let reply;
    if (interactive) {
      // opencode parity: the model's answer is the reply. Never reframe a delivered
      // answer as a blocked coding task. Fall back to a gentle notice only when the
      // turn produced no output at all.
      reply = (result.output || result.stderr || '')
        || (outcome.reason === 'agent_finished_without_answer'
          ? makeIncompleteReadOnlyReply({ output: result.output, stderr: result.stderr })
          : '');
    } else {
      reply = outcome.status === 'blocked'
        ? makeBlockedReply({ output: result.output || result.stderr || '', reason: outcome.reason })
        : outcome.reason === 'agent_finished_without_answer'
          ? makeIncompleteReadOnlyReply({ output: result.output, stderr: result.stderr })
          : (result.output || result.stderr || '');
    }
    const response = {
      ok: outcome.success,
      success: outcome.success,
      reply,
      output: result.output || '',
      stderr: result.stderr || '',
      model: result.model || providerRoute.model || request.model || '',
      provider: result.provider || providerRoute.providerClient?.type || request.provider || '',
      latencyMs: this.now() - startedAt,
      tokens: result.usage || {},
      cost: result.cost || 0,
      toolCalls: collectToolCalls(result),
      log: result.log || [],
      changedFiles: outcome.changedFiles,
      codingRunId,
      completion: outcome,
      outcome,
      runtimeMode: result.runtimeMode || '',
      sessionId: result.sessionId || codingRunId,
    };
    progress({
      type: 'coding_outcome',
      codingRunId,
      status: outcome.status,
      success: outcome.success,
      reason: outcome.reason,
      changedFiles: outcome.changedFiles,
      verified: outcome.verified,
      latencyMs: response.latencyMs,
    });
    return response;
  }
}

module.exports = {
  CodingRunController,
  classifyCodingTurnIntent,
  deriveCodingOutcome,
  resolveCodingProvider,
  collectToolCalls,
  hasDeliveredFinalAnswer,
};
