'use strict';

let dsmlCallSeq = 0;

const DSML_MARKER = '(?:[|｜]\\s*){1,2}DSML\\s*(?:[|｜]\\s*){1,2}';
const DSML_OPEN = `<\\s*${DSML_MARKER}\\s*`;
const DSML_CLOSE = `<\\/\\s*${DSML_MARKER}\\s*`;
const DSML_TOOL_CALLS_START_RE = new RegExp(`${DSML_OPEN}tool_calls\\s*>`, 'i');
const DSML_TOOL_CALLS_RE = new RegExp(`${DSML_OPEN}tool_calls\\s*>[\\s\\S]*?${DSML_CLOSE}tool_calls\\s*>`, 'gi');
const DSML_INVOKE_RE = new RegExp(`${DSML_OPEN}invoke\\s+([^>]*)>([\\s\\S]*?)${DSML_CLOSE}invoke\\s*>`, 'gi');
const DSML_PARAMETER_RE = new RegExp(`${DSML_OPEN}parameter\\s+([^>]*)>([\\s\\S]*?)${DSML_CLOSE}parameter\\s*>`, 'gi');
const DSML_ANY_TOOL_MARKUP_RE = new RegExp(`${DSML_OPEN}(?:tool_calls|invoke|parameter)\\b|${DSML_CLOSE}(?:tool_calls|invoke|parameter)\\b|invoke\\s+name=|tool_calls\\s*>`, 'i');
const ATTR_RE = /([A-Za-z_][A-Za-z0-9_-]*)="([^"]*)"/g;

function parseAttrs(text = '') {
  const attrs = {};
  for (const match of String(text).matchAll(ATTR_RE)) {
    attrs[match[1]] = match[2];
  }
  return attrs;
}

function coerceValue(value, attrs = {}) {
  const raw = String(value ?? '');
  const stringAttr = String(attrs.string ?? '').trim().toLowerCase();
  if (stringAttr === 'true') return raw;

  const trimmed = raw.trim();
  if (trimmed === 'true') return true;
  if (trimmed === 'false') return false;
  if (stringAttr === 'false' && trimmed) {
    try {
      return JSON.parse(trimmed);
    } catch {}
  }
  return value;
}

function parseDsmlToolCallBlocks(content) {
  const text = typeof content === 'string' ? content : '';
  if (!text || !DSML_TOOL_CALLS_START_RE.test(text)) {
    return { content, toolCalls: [] };
  }

  const toolCalls = [];
  const withoutToolBlocks = text.replace(DSML_TOOL_CALLS_RE, (block) => {
    for (const invokeMatch of block.matchAll(DSML_INVOKE_RE)) {
      const invokeAttrs = parseAttrs(invokeMatch[1]);
      const name = invokeAttrs.name;
      if (!name) continue;

      const input = {};
      for (const paramMatch of invokeMatch[2].matchAll(DSML_PARAMETER_RE)) {
        const paramAttrs = parseAttrs(paramMatch[1]);
        const paramName = paramAttrs.name;
        if (!paramName) continue;
        input[paramName] = coerceValue(paramMatch[2], paramAttrs);
      }

      toolCalls.push({
        id: `call_dsml_${++dsmlCallSeq}`,
        name,
        input,
      });
    }
    return '';
  });

  return {
    content: withoutToolBlocks.trim() || null,
    toolCalls,
  };
}

function firstDefined(input, keys) {
  for (const key of keys) {
    if (input[key] != null && input[key] !== '') return input[key];
  }
  return undefined;
}

function assignAlias(input, canonical, aliases) {
  if (input[canonical] != null && input[canonical] !== '') return;
  const value = firstDefined(input, aliases);
  if (value !== undefined) input[canonical] = value;
}

function normalizeEditAliases(edit) {
  if (!edit || typeof edit !== 'object') return edit;
  const normalized = { ...edit };
  assignAlias(normalized, 'old_string', ['oldString', 'old', 'find']);
  assignAlias(normalized, 'new_string', ['newString', 'new', 'replace']);
  assignAlias(normalized, 'replace_all', ['replaceAll', 'replace_all']);
  return normalized;
}

function commandBasename(command) {
  const text = String(command || '').trim();
  if (!text) return '';
  return text.split(/\s+/)[0].split(/[\\/]/).pop().toLowerCase();
}

function looksLikeShellScript(text) {
  const script = String(text || '');
  if (!script) return false;
  return /<<\s*['"]?[A-Za-z0-9_ -]+['"]?/.test(script)
    || /(^|\n)\s*cat\s+>\s+\S+/.test(script)
    || /(^|\n)\s*(cd|mkdir|cp|mv|rm|touch|npm|node|python3?|git|curl)\b[\s\S]*(\|\||&&|[<>])/.test(script)
    || /(^|\n)\s*(for|while)\s+.+;\s*do\b/.test(script);
}

function normalizeRunShellInput(input) {
  if (!input || typeof input !== 'object') return input;
  const normalized = { ...input };
  const commandName = commandBasename(normalized.command);
  const args = Array.isArray(normalized.args) ? normalized.args : [];
  if (args.length >= 2 && args[0] === '-c' && typeof args[1] === 'string') {
    const script = args[1];
    const shellCommands = new Set(['sh', 'bash', 'zsh', 'fish']);
    const interpreters = new Set(['python', 'python3', 'node', 'ruby', 'perl']);
    if (shellCommands.has(commandName) || (interpreters.has(commandName) && looksLikeShellScript(script))) {
      normalized.command = script;
      delete normalized.args;
    }
  }
  return normalized;
}

function normalizeToolCall(call) {
  const input = call && typeof call.input === 'object' && call.input !== null ? { ...call.input } : {};
  let name = call?.name;

  assignAlias(input, 'file_path', ['filePath', 'filepath', 'file', 'filename']);
  assignAlias(input, 'old_string', ['oldString', 'old', 'find']);
  assignAlias(input, 'new_string', ['newString', 'new', 'replace']);
  assignAlias(input, 'replace_all', ['replaceAll']);
  assignAlias(input, 'max_bytes', ['maxBytes', 'maxbytes']);
  assignAlias(input, 'timeout_ms', ['timeoutMs', 'timeout']);

  if (Array.isArray(input.edits)) {
    input.edits = input.edits.map(normalizeEditAliases);
  }

  if (input.path && !input.directory) input.directory = input.path;
  if ((name === 'read_file' || name === 'write_file' || name === 'edit_file' || name === 'multi_edit') && input.path && !input.file_path) {
    input.file_path = input.path;
  }
  if (name === 'list_directory' && input.path && !input.directory) {
    input.directory = input.path;
  }
  if (name === 'grep_files' && input.query && !input.pattern) {
    input.pattern = input.query;
  }

  // DeepSeek occasionally emits a file-glob request as `search_files` with
  // OpenCode-style `{ path, pattern, recursive }` args. Wall-E's `search_files`
  // is Spotlight query based, so run the user's intended filesystem glob.
  if (name === 'search_files' && !input.query && input.pattern) {
    name = 'glob';
  }

  if (name === 'glob') {
    if (!input.pattern) input.pattern = input.query || '*';
    delete input.query;
  } else if (name === 'search_files') {
    if (!input.query && input.pattern) input.query = input.pattern;
  }

  if (name === 'run_shell') {
    const shellInput = normalizeRunShellInput(input);
    for (const key of Object.keys(input)) delete input[key];
    Object.assign(input, shellInput);
  }

  delete input.path;
  delete input.recursive;

  return {
    ...call,
    name,
    input,
  };
}

function stripDsmlToolMarkup(content) {
  const text = typeof content === 'string' ? content : '';
  if (!text) return content;
  const withoutCompleteBlocks = text.replace(DSML_TOOL_CALLS_RE, '');
  const partialStart = withoutCompleteBlocks.search(DSML_TOOL_CALLS_START_RE);
  if (partialStart >= 0) return withoutCompleteBlocks.slice(0, partialStart).trim() || null;
  return withoutCompleteBlocks.trim() || null;
}

function hasTextToolCallMarkup(content) {
  return typeof content === 'string' && DSML_ANY_TOOL_MARKUP_RE.test(content);
}

function extractTextToolCalls(response = {}) {
  const existing = Array.isArray(response.toolCalls) ? response.toolCalls : [];

  const parsed = parseDsmlToolCallBlocks(response.content);
  if (!parsed.toolCalls.length) {
    if (!hasTextToolCallMarkup(response.content)) return response;
    return {
      ...response,
      content: stripDsmlToolMarkup(response.content),
      toolCalls: existing.map(normalizeToolCall),
      textToolCallFormat: 'dsml_incomplete',
      recoveredTextToolCalls: [],
      incompleteTextToolCallMarkup: true,
    };
  }
  const parsedToolCalls = parsed.toolCalls.map(normalizeToolCall);

  return {
    ...response,
    content: parsed.content,
    toolCalls: existing.map(normalizeToolCall).concat(parsedToolCalls),
    stopReason: response.stopReason === 'end_turn' ? 'tool_use' : (response.stopReason || 'tool_use'),
    textToolCallFormat: 'dsml',
    recoveredTextToolCalls: parsedToolCalls,
  };
}

function recoverAllowedTextToolCalls(response = {}, tools = []) {
  const parsed = extractTextToolCalls(response);
  if (!parsed?.textToolCallFormat) return parsed;

  const allowedToolNames = new Set((Array.isArray(tools) ? tools : [])
    .map((tool) => tool?.name)
    .filter(Boolean));
  const recovered = Array.isArray(parsed.recoveredTextToolCalls) ? parsed.recoveredTextToolCalls : [];
  const recoveredIds = new Set(recovered.map((call) => call.id).filter(Boolean));
  const nativeCalls = (Array.isArray(parsed.toolCalls) ? parsed.toolCalls : [])
    .filter((call) => !call?.id || !recoveredIds.has(call.id));
  const allowedRecovered = recovered.filter((call) => allowedToolNames.has(call.name));
  const droppedRecovered = recovered.filter((call) => !allowedToolNames.has(call.name));
  const toolCalls = nativeCalls.concat(allowedRecovered);

  return {
    ...parsed,
    content: parsed.content,
    toolCalls,
    recoveredTextToolCalls: allowedRecovered,
    droppedTextToolCalls: droppedRecovered,
    stopReason: toolCalls.length > 0 ? parsed.stopReason : (response.stopReason || 'end_turn'),
  };
}

module.exports = {
  extractAllowedTextToolCalls: recoverAllowedTextToolCalls,
  extractTextToolCalls,
  hasTextToolCallMarkup,
  parseDsmlToolCallBlocks,
  normalizeParsedTextToolCall: normalizeToolCall,
  normalizeToolCall,
  recoverAllowedTextToolCalls,
};
