// ---------------------------------------------------------------------------
// taskReceiver -- pulls external tasks from Hub, auto-claims, and injects
// them as high-priority signals into the evolution loop.
//
// v2: Smart task selection with difficulty-aware ROI scoring and capability
//     matching via memory graph history.
// ---------------------------------------------------------------------------

const { getNodeId, buildHubHeaders, buildNodeScopedHubHeaders } = require('./a2aProtocol');
const { resolveHubUrl } = require('../config');
const { hubFetch } = require('./hubFetch');
const { createTask, validateTask } = require('./schemas/task');

function buildAuthHeaders() {
  const buildHeaders = buildNodeScopedHubHeaders || buildHubHeaders;
  return buildHeaders();
}

const TASK_STRATEGY = String(process.env.TASK_STRATEGY || 'balanced').toLowerCase();
const TASK_MIN_CAPABILITY_MATCH = Number(process.env.TASK_MIN_CAPABILITY_MATCH) || 0.1;

// Scoring weights by strategy
const STRATEGY_WEIGHTS = {
  greedy:       { roi: 0.10, capability: 0.05, completion: 0.05, bounty: 0.80 },
  balanced:     { roi: 0.35, capability: 0.30, completion: 0.20, bounty: 0.15 },
  conservative: { roi: 0.25, capability: 0.45, completion: 0.25, bounty: 0.05 },
};

/**
 * Fetch available tasks from Hub via the A2A fetch endpoint.
 * Optionally piggybacks proactive questions in the payload for Hub to create bounties.
 *
 * @param {object} [opts]
 * @param {Array<{ question: string, amount?: number, signals?: string[] }>} [opts.questions]
 * @returns {{ tasks: Array, questions_created?: Array }}
 */
async function fetchTasks(opts) {
  const o = opts || {};
  const nodeId = getNodeId();
  if (!nodeId) return { tasks: [] };

  try {
    const payload = {
      tasks_only: true,
      include_tasks: true,
    };

    if (Array.isArray(o.questions) && o.questions.length > 0) {
      payload.questions = o.questions;
    }

    const msg = {
      protocol: 'gep-a2a',
      protocol_version: '1.0.0',
      message_type: 'fetch',
      message_id: `msg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
      sender_id: nodeId,
      timestamp: new Date().toISOString(),
      payload,
    };

    const url = `${resolveHubUrl().replace(/\/+$/, '')}/a2a/fetch`;
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), 8000);

    const res = await hubFetch(url, {
      method: 'POST',
      headers: buildAuthHeaders(),
      body: JSON.stringify(msg),
      signal: controller.signal,
    });
    clearTimeout(timer);

    if (!res.ok) return { tasks: [] };

    const data = await res.json();
    const respPayload = data.payload || data;
    const tasks = Array.isArray(respPayload.tasks)
      ? respPayload.tasks.map(function(t) { return createTask(t); }).filter(function(t) {
          try { return validateTask(t); } catch (e) {
            console.warn('[TaskReceiver] dropping invalid task:', e.message, t.task_id || '(no id)');
            return false;
          }
        })
      : [];
    const result = { tasks };

    if (respPayload.questions_created) {
      result.questions_created = respPayload.questions_created;
    }

    // LessonL: extract relevant lessons from Hub response
    if (Array.isArray(respPayload.relevant_lessons) && respPayload.relevant_lessons.length > 0) {
      result.relevant_lessons = respPayload.relevant_lessons;
    }

    return result;
  } catch (err) {
    console.warn("[TaskReceiver] fetchTasks failed:", err && err.message ? err.message : err);
    return { tasks: [] };
  }
}

// ---------------------------------------------------------------------------
// Capability matching: how well this agent's history matches a task's signals
// ---------------------------------------------------------------------------

function parseSignals(raw) {
  if (!raw) return [];
  return String(raw).split(',').map(function(s) { return s.trim().toLowerCase(); }).filter(Boolean);
}

function jaccard(a, b) {
  if (!a.length || !b.length) return 0;
  var setA = new Set(a);
  var setB = new Set(b);
  var inter = 0;
  for (var v of setB) { if (setA.has(v)) inter++; }
  return inter / (setA.size + setB.size - inter);
}

/**
 * Estimate how well this agent can handle a task based on memory graph history.
 * Returns 0.0 - 1.0 where 1.0 = strong match with high success rate.
 *
 * @param {object} task - task from Hub (has .signals field)
 * @param {Array} memoryEvents - from tryReadMemoryGraphEvents()
 * @returns {number}
 */
function estimateCapabilityMatch(task, memoryEvents) {
  if (!Array.isArray(memoryEvents) || memoryEvents.length === 0) return 0.5;

  var taskSignals = parseSignals(task.signals || task.title);
  if (taskSignals.length === 0) return 0.5;

  var successBySignalKey = {};
  var totalBySignalKey = {};
  var allSignals = {};

  for (var i = 0; i < memoryEvents.length; i++) {
    var ev = memoryEvents[i];
    if (!ev || ev.type !== 'MemoryGraphEvent' || ev.kind !== 'outcome') continue;

    var sigs = (ev.signal && Array.isArray(ev.signal.signals)) ? ev.signal.signals : [];
    var key = (ev.signal && ev.signal.key) ? String(ev.signal.key) : '';
    var status = (ev.outcome && ev.outcome.status) ? String(ev.outcome.status) : '';

    for (var j = 0; j < sigs.length; j++) {
      allSignals[sigs[j].toLowerCase()] = true;
    }

    if (!key) continue;
    if (!totalBySignalKey[key]) { totalBySignalKey[key] = 0; successBySignalKey[key] = 0; }
    totalBySignalKey[key]++;
    if (status === 'success') successBySignalKey[key]++;
  }

  // Jaccard overlap between task signals and all signals this agent has worked with
  var allSigArr = Object.keys(allSignals);
  var overlapScore = jaccard(taskSignals, allSigArr);

  // Weighted success rate across matching signal keys
  var weightedSuccess = 0;
  var weightSum = 0;
  for (var sk in totalBySignalKey) {
    // Reconstruct signals from the key for comparison
    var skParts = sk.split('|').map(function(s) { return s.trim().toLowerCase(); }).filter(Boolean);
    var sim = jaccard(taskSignals, skParts);
    if (sim < 0.15) continue;

    var total = totalBySignalKey[sk];
    var succ = successBySignalKey[sk] || 0;
    var rate = (succ + 1) / (total + 2); // Laplace smoothing
    weightedSuccess += rate * sim;
    weightSum += sim;
  }

  var successScore = weightSum > 0 ? (weightedSuccess / weightSum) : 0.5;

  // Combine: 60% success rate history + 40% signal overlap
  return Math.min(1, overlapScore * 0.4 + successScore * 0.6);
}

// ---------------------------------------------------------------------------
// Local fallback difficulty estimation when Hub doesn't provide complexity_score
// ---------------------------------------------------------------------------

function localDifficultyEstimate(task) {
  var signals = parseSignals(task.signals);
  var signalFactor = Math.min(signals.length / 8, 1);

  var titleWords = (task.title || '').split(/\s+/).filter(Boolean).length;
  var titleFactor = Math.min(titleWords / 15, 1);

  return Math.min(1, signalFactor * 0.6 + titleFactor * 0.4);
}

// ---------------------------------------------------------------------------
// Commitment deadline estimation -- based on task difficulty
// ---------------------------------------------------------------------------

const MIN_COMMITMENT_MS = 5 * 60 * 1000;       // 5 min (Hub minimum)
const MAX_COMMITMENT_MS = 24 * 60 * 60 * 1000;  // 24 h  (Hub maximum)

const DIFFICULTY_DURATION_MAP = [
  { threshold: 0.3, durationMs: 15 * 60 * 1000 },   // low:       15 min
  { threshold: 0.5, durationMs: 30 * 60 * 1000 },   // medium:    30 min
  { threshold: 0.7, durationMs: 60 * 60 * 1000 },   // high:      60 min
  { threshold: 1.0, durationMs: 120 * 60 * 1000 },  // very high: 120 min
];

/**
 * Estimate a reasonable commitment deadline for a task.
 * Returns an ISO-8601 date string or null if estimation fails.
 *
 * @param {object} task - task from Hub
 * @returns {string|null}
 */
function estimateCommitmentDeadline(task) {
  if (!task) return null;

  var difficulty = (task.complexity_score != null)
    ? Number(task.complexity_score)
    : localDifficultyEstimate(task);

  var durationMs = DIFFICULTY_DURATION_MAP[DIFFICULTY_DURATION_MAP.length - 1].durationMs;
  for (var i = 0; i < DIFFICULTY_DURATION_MAP.length; i++) {
    if (difficulty <= DIFFICULTY_DURATION_MAP[i].threshold) {
      durationMs = DIFFICULTY_DURATION_MAP[i].durationMs;
      break;
    }
  }

  durationMs = Math.max(MIN_COMMITMENT_MS, Math.min(MAX_COMMITMENT_MS, durationMs));

  var deadline = new Date(Date.now() + durationMs);

  if (task.expires_at) {
    var expiresAt = new Date(task.expires_at);
    if (!isNaN(expiresAt.getTime()) && expiresAt < deadline) {
      var remaining = expiresAt.getTime() - Date.now();
      if (remaining < MIN_COMMITMENT_MS) return null;
      var adjusted = new Date(expiresAt.getTime() - 60000);
      if (adjusted.getTime() - Date.now() < MIN_COMMITMENT_MS) return null;
      deadline = adjusted;
    }
  }

  return deadline.toISOString();
}

// ---------------------------------------------------------------------------
// Score a single task for this agent
// ---------------------------------------------------------------------------

/**
 * @param {object} task - task from Hub
 * @param {number} capabilityMatch - from estimateCapabilityMatch()
 * @returns {{ composite: number, factors: object }}
 */
function scoreTask(task, capabilityMatch) {
  var w = STRATEGY_WEIGHTS[TASK_STRATEGY] || STRATEGY_WEIGHTS.balanced;

  var difficulty = (task.complexity_score != null) ? task.complexity_score : localDifficultyEstimate(task);
  var bountyAmount = task.bounty_amount || 0;
  var completionRate = (task.historical_completion_rate != null) ? task.historical_completion_rate : 0.5;

  // ROI: bounty per unit difficulty (higher = better value)
  var roiRaw = bountyAmount / (difficulty + 0.1);
  var roiNorm = Math.min(roiRaw / 200, 1); // normalize: 200-credit ROI = max

  // Bounty absolute: normalize against a reference max
  var bountyNorm = Math.min(bountyAmount / 100, 1);

  var composite =
    w.roi * roiNorm +
    w.capability * capabilityMatch +
    w.completion * completionRate +
    w.bounty * bountyNorm;

  return {
    composite: Math.round(composite * 1000) / 1000,
    factors: {
      roi: Math.round(roiNorm * 100) / 100,
      capability: Math.round(capabilityMatch * 100) / 100,
      completion: Math.round(completionRate * 100) / 100,
      bounty: Math.round(bountyNorm * 100) / 100,
      difficulty: Math.round(difficulty * 100) / 100,
    },
  };
}

// ---------------------------------------------------------------------------
// Enhanced task selection with scoring
// ---------------------------------------------------------------------------

/**
 * Pick the best task from a list using composite scoring.
 * @param {Array} tasks
 * @param {Array} [memoryEvents] - from tryReadMemoryGraphEvents()
 * @returns {object|null}
 */
function selectBestTask(tasks, memoryEvents) {
  if (!Array.isArray(tasks) || tasks.length === 0) return null;

  var nodeId = getNodeId();

  // Already-claimed tasks for this node always take top priority (resume work)
  var myClaimedTask = tasks.find(function(t) {
    return t.status === 'claimed' && t.claimed_by === nodeId;
  });
  if (myClaimedTask) return myClaimedTask;

  // Filter to open tasks only
  var open = tasks.filter(function(t) { return t.status === 'open'; });
  if (open.length === 0) return null;

  // Legacy greedy mode: preserve old behavior exactly
  if (TASK_STRATEGY === 'greedy' && (!memoryEvents || memoryEvents.length === 0)) {
    var bountyTasks = open.filter(function(t) { return t.bounty_id; });
    if (bountyTasks.length > 0) {
      bountyTasks.sort(function(a, b) { return (b.bounty_amount || 0) - (a.bounty_amount || 0); });
      return bountyTasks[0];
    }
    return open[0];
  }

  // Score all open tasks
  var scored = open.map(function(t) {
    var cap = estimateCapabilityMatch(t, memoryEvents || []);
    var result = scoreTask(t, cap);
    return { task: t, composite: result.composite, factors: result.factors, capability: cap };
  });

  // Filter by minimum capability match (unless conservative skipping is off)
  if (TASK_MIN_CAPABILITY_MATCH > 0) {
    var filtered = scored.filter(function(s) { return s.capability >= TASK_MIN_CAPABILITY_MATCH; });
    if (filtered.length > 0) scored = filtered;
  }

  scored.sort(function(a, b) { return b.composite - a.composite; });

  // Log top 3 candidates for debugging
  var top3 = scored.slice(0, 3);
  for (var i = 0; i < top3.length; i++) {
    var s = top3[i];
    console.log('[TaskStrategy] #' + (i + 1) + ' "' + (s.task.title || s.task.task_id || '').slice(0, 50) + '" score=' + s.composite + ' ' + JSON.stringify(s.factors));
  }

  return scored[0] ? scored[0].task : null;
}

/**
 * Claim a task on the Hub.
 * @param {string} taskId
 * @param {{ commitment_deadline?: string }} [opts]
 * @returns {boolean} true if claim succeeded
 */
async function claimTask(taskId, opts) {
  const nodeId = getNodeId();
  if (!nodeId || !taskId) return false;

  try {
    const url = `${resolveHubUrl().replace(/\/+$/, '')}/a2a/task/claim`;
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), 5000);

    const body = { task_id: taskId, node_id: nodeId };
    if (opts && opts.commitment_deadline) {
      body.commitment_deadline = opts.commitment_deadline;
    }

    const res = await hubFetch(url, {
      method: 'POST',
      headers: buildAuthHeaders(),
      body: JSON.stringify(body),
      signal: controller.signal,
    });
    clearTimeout(timer);

    return res.ok;
  } catch {
    return false;
  }
}

/**
 * Complete a task on the Hub with the result asset ID.
 * @param {string} taskId
 * @param {string} assetId
 * @returns {boolean}
 */
async function completeTask(taskId, assetId) {
  const nodeId = getNodeId();
  if (!nodeId || !taskId || !assetId) return false;

  try {
    const url = `${resolveHubUrl().replace(/\/+$/, '')}/a2a/task/complete`;
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), 5000);

    const res = await hubFetch(url, {
      method: 'POST',
      headers: buildAuthHeaders(),
      body: JSON.stringify({ task_id: taskId, asset_id: assetId, node_id: nodeId }),
      signal: controller.signal,
    });
    clearTimeout(timer);

    return res.ok;
  } catch {
    return false;
  }
}

/**
 * Extract signals from a task to inject into evolution cycle.
 * @param {object} task
 * @returns {string[]} signals array
 */
function taskToSignals(task) {
  if (!task) return [];
  const signals = [];
  if (task.signals) {
    const parts = String(task.signals).split(',').map(s => s.trim()).filter(Boolean);
    signals.push(...parts);
  }
  if (task.title) {
    const words = String(task.title).toLowerCase().split(/\s+/).filter(w => w.length >= 3);
    for (const w of words.slice(0, 5)) {
      if (!signals.includes(w)) signals.push(w);
    }
  }
  signals.push('external_task');
  if (task.bounty_id) signals.push('bounty_task');
  return signals;
}

// ---------------------------------------------------------------------------
// Worker Pool task operations (POST /a2a/work/*)
// These use a separate API from bounty tasks and return assignment objects.
// ---------------------------------------------------------------------------

async function claimWorkerTask(taskId) {
  const nodeId = getNodeId();
  if (!nodeId || !taskId) return null;

  try {
    const url = `${resolveHubUrl().replace(/\/+$/, '')}/a2a/work/claim`;
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), 5000);

    const res = await hubFetch(url, {
      method: 'POST',
      headers: buildAuthHeaders(),
      body: JSON.stringify({ task_id: taskId, node_id: nodeId }),
      signal: controller.signal,
    });
    clearTimeout(timer);

    if (!res.ok) return null;
    return await res.json();
  } catch {
    return null;
  }
}

async function completeWorkerTask(assignmentId, resultAssetId) {
  const nodeId = getNodeId();
  if (!nodeId || !assignmentId || !resultAssetId) return false;

  try {
    const url = `${resolveHubUrl().replace(/\/+$/, '')}/a2a/work/complete`;
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), 5000);

    const res = await hubFetch(url, {
      method: 'POST',
      headers: buildAuthHeaders(),
      body: JSON.stringify({ assignment_id: assignmentId, node_id: nodeId, result_asset_id: resultAssetId }),
      signal: controller.signal,
    });
    clearTimeout(timer);

    return res.ok;
  } catch {
    return false;
  }
}

/**
 * Atomic claim+complete for deferred worker tasks.
 * Called from solidify after a successful evolution cycle so we never hold
 * an assignment that might expire before completion.
 *
 * @param {string} taskId
 * @param {string} resultAssetId - sha256:... of the published capsule
 * @returns {{ ok: boolean, assignment_id?: string, error?: string }}
 */
async function claimAndCompleteWorkerTask(taskId, resultAssetId) {
  const nodeId = getNodeId();
  if (!nodeId || !taskId || !resultAssetId) {
    return { ok: false, error: 'missing_params' };
  }

  const assignment = await claimWorkerTask(taskId);
  if (!assignment) {
    return { ok: false, error: 'claim_failed' };
  }

  const assignmentId = assignment.id || assignment.assignment_id;
  if (!assignmentId) {
    return { ok: false, error: 'no_assignment_id' };
  }

  const completed = await completeWorkerTask(assignmentId, resultAssetId);
  if (!completed) {
    console.warn(`[WorkerPool] Claimed assignment ${assignmentId} but complete failed -- will expire on Hub`);
    return { ok: false, error: 'complete_failed', assignment_id: assignmentId };
  }

  return { ok: true, assignment_id: assignmentId };
}

// ---------------------------------------------------------------------------
// PRIVACY_PARAMS detection -- checks if a task body contains a sealed
// computing block and extracts tool/blob references for the evolution loop.
// ---------------------------------------------------------------------------

/**
 * Check if a task requires privacy/sealed computing.
 * @param {object} task
 * @returns {{ toolId: string, blobIds: string[] } | null}
 */
function detectPrivacyTask(task) {
  if (!task) return null;
  const body = task.body || task.description || '';
  try {
    const { parsePrivacyParams } = require('./privacyClient');
    return parsePrivacyParams(body);
  } catch {
    return null;
  }
}

/**
 * Augment taskToSignals output with privacy-specific signals when applicable.
 * @param {object} task
 * @returns {string[]}
 */
function taskToSignalsWithPrivacy(task) {
  const signals = taskToSignals(task);
  const pp = detectPrivacyTask(task);
  if (pp) {
    if (!signals.includes('privacy_computing')) signals.push('privacy_computing');
    if (!signals.includes('sealed_tool')) signals.push('sealed_tool');
  }
  return signals;
}

module.exports = {
  fetchTasks,
  selectBestTask,
  estimateCapabilityMatch,
  scoreTask,
  claimTask,
  completeTask,
  taskToSignals,
  taskToSignalsWithPrivacy,
  detectPrivacyTask,
  claimWorkerTask,
  completeWorkerTask,
  claimAndCompleteWorkerTask,
  estimateCommitmentDeadline,
};
