'use strict';

const { normalizeAgentMode } = require('./contract');
const { isSafeRelativePath } = require('./utils');

function normalizeScopePath(scope) {
  let rel = String(scope || '').trim().replace(/\\/g, '/');
  rel = rel.replace(/\/+/g, '/').replace(/^\.\//, '').replace(/\/$/, '');
  if (!rel || rel === '*') return '.';
  return isSafeRelativePath(rel) ? rel : null;
}

function normalizeWriteScope(writeScope) {
  const values = Array.isArray(writeScope) ? writeScope : (writeScope ? [writeScope] : ['.']);
  const scopes = values.map(normalizeScopePath).filter(Boolean);
  return [...new Set(scopes.length > 0 ? scopes : ['.'])];
}

function isPathInScope(relPath, writeScope) {
  const rel = normalizeScopePath(relPath);
  if (!rel) return false;
  const scopes = normalizeWriteScope(writeScope);
  return scopes.some(scope => scope === '.' || rel === scope || rel.startsWith(`${scope}/`));
}

function scopesOverlap(scopeA, scopeB) {
  const aScopes = normalizeWriteScope(scopeA);
  const bScopes = normalizeWriteScope(scopeB);
  return aScopes.some(a => bScopes.some(b => (
    a === '.' || b === '.' || a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`)
  )));
}

function normalizeAssignment(assignment = {}, index = 0) {
  const id = assignment.id || assignment.assignmentId || `agent-${index + 1}`;
  return {
    ...assignment,
    id,
    runnerId: assignment.runnerId || assignment.agent || 'claude-code',
    prompt: String(assignment.prompt || ''),
    mode: normalizeAgentMode(assignment.mode || 'build'),
    writeScope: normalizeWriteScope(assignment.writeScope),
  };
}

function findOverlappingWriteScopes(assignments = []) {
  const normalized = assignments.map(normalizeAssignment);
  const overlaps = [];
  for (let i = 0; i < normalized.length; i += 1) {
    for (let j = i + 1; j < normalized.length; j += 1) {
      if (scopesOverlap(normalized[i].writeScope, normalized[j].writeScope)) {
        overlaps.push({
          left: normalized[i].id,
          right: normalized[j].id,
          leftScope: normalized[i].writeScope,
          rightScope: normalized[j].writeScope,
        });
      }
    }
  }
  return overlaps;
}

function hasOverlappingWriteScopes(assignments = []) {
  return findOverlappingWriteScopes(assignments).length > 0;
}

function buildScopedPrompt(assignment) {
  const scopeText = assignment.writeScope.join(', ');
  return [
    `Assignment: ${assignment.id}`,
    `Write scope: ${scopeText}`,
    'Only modify files inside the write scope unless explicitly instructed by WallE.',
    '',
    assignment.prompt,
  ].join('\n');
}

async function runOneAssignment(assignment, opts = {}) {
  const runTask = opts.runTask;
  const result = await runTask(assignment.runnerId, {
    ...(opts.taskOptions || {}),
    ...(assignment.options || {}),
    prompt: buildScopedPrompt(assignment),
    cwd: assignment.cwd || opts.cwd,
    timeoutMs: assignment.timeoutMs || opts.timeoutMs,
    model: assignment.model || opts.model,
    mode: assignment.mode,
    sessionId: assignment.sessionId,
    writeScope: assignment.writeScope,
  });

  const changedFiles = Array.isArray(result.changedFiles) ? result.changedFiles : [];
  const outOfScopeFiles = changedFiles.filter(file => !isPathInScope(file, assignment.writeScope));
  const scopeViolation = outOfScopeFiles.length > 0;

  return {
    assignmentId: assignment.id,
    runnerId: result.runnerId || assignment.runnerId,
    providerType: result.providerType || assignment.providerType || 'unknown',
    model: result.model || assignment.model,
    mode: assignment.mode,
    writeScope: assignment.writeScope,
    success: Boolean(result.success) && !scopeViolation,
    output: result.output || '',
    stderr: result.stderr || '',
    exitCode: Number.isInteger(result.exitCode) ? result.exitCode : (result.success ? 0 : -1),
    sessionId: result.sessionId || assignment.sessionId,
    resumeHandle: result.resumeHandle,
    latencyMs: result.latencyMs || null,
    events: Array.isArray(result.events) ? result.events : [],
    changedFiles,
    outOfScopeFiles,
    scopeViolation,
    diff: result.diff || '',
    usage: result.usage || null,
    costEstimate: result.costEstimate || null,
    error: scopeViolation ? `Changed files outside write scope: ${outOfScopeFiles.join(', ')}` : (result.error || ''),
  };
}

async function runControlledMultiAgent(assignments, opts = {}) {
  const normalized = Array.isArray(assignments) ? assignments.map(normalizeAssignment) : [];
  const parallel = opts.parallel === true;
  const runTask = opts.runTask;

  if (!runTask || typeof runTask !== 'function') {
    throw new Error('runControlledMultiAgent requires a runTask function');
  }
  if (normalized.length === 0) {
    return {
      success: false,
      mode: parallel ? 'parallel' : 'serial',
      refused: true,
      error: 'No agent assignments were provided',
      overlaps: [],
      proposals: [],
    };
  }

  const overlaps = findOverlappingWriteScopes(normalized);
  if (parallel && overlaps.length > 0 && !opts.allowOverlaps) {
    return {
      success: false,
      mode: 'parallel',
      refused: true,
      error: 'Parallel multi-agent execution refused because write scopes overlap',
      overlaps,
      proposals: [],
    };
  }

  const run = assignment => runOneAssignment(assignment, { ...opts, runTask })
    .catch(err => ({
      assignmentId: assignment.id,
      runnerId: assignment.runnerId,
      providerType: assignment.providerType || 'unknown',
      mode: assignment.mode,
      writeScope: assignment.writeScope,
      success: false,
      output: '',
      stderr: err.message,
      exitCode: -1,
      sessionId: assignment.sessionId,
      events: [],
      changedFiles: [],
      outOfScopeFiles: [],
      scopeViolation: false,
      diff: '',
      usage: null,
      costEstimate: null,
      error: err.message,
    }));

  const proposals = [];
  if (parallel) {
    proposals.push(...await Promise.all(normalized.map(run)));
  } else {
    for (const assignment of normalized) {
      proposals.push(await run(assignment));
    }
  }

  return {
    success: proposals.every(proposal => proposal.success),
    mode: parallel ? 'parallel' : 'serial',
    refused: false,
    error: proposals.find(proposal => !proposal.success)?.error || '',
    overlaps,
    proposals,
  };
}

module.exports = {
  normalizeWriteScope,
  isPathInScope,
  scopesOverlap,
  normalizeAssignment,
  findOverlappingWriteScopes,
  hasOverlappingWriteScopes,
  runControlledMultiAgent,
};
