/**
 * PR-37 Delivery Planning — Batch Planner (Part H)
 *
 * Generates 4 batch types based on dependency analysis:
 *   - DevelopmentBatch: Group stories by dependency level for parallel dev
 *   - TestingBatch: Group completed dev batches ready for testing
 *   - ReleaseBatch: Group features ready for staged deployment
 *   - DeliveryBatch: Customer-facing delivery increments
 *
 * Each batch captures parallel groups, prerequisite batches, and story/feature/epic references.
 *
 * @module pr-37-delivery-planning/batch-planner
 * @since PR-37
 */

let _batchSeq = 0;

/**
 * Generate a complete batch plan from SF-02 output and sprint/release plans.
 *
 * @param {object} opts
 * @param {object[]} opts.stories — User stories (from SF-02)
 * @param {object} opts.dependencies — Dependency graph { nodes, edges }
 * @param {object[]} opts.priorities — Priority matrix entries
 * @param {object} opts.sprints — Sprint plan (from sprint-planner)
 * @param {object} opts.releases — Release plan (from release-planner)
 * @returns {object} Batch plan { batches, summary }
 *
 * @example
 * ```js
 * const batchPlan = generateBatchPlan({
 *   stories: myStories,
 *   dependencies: myGraph,
 *   priorities: myPriorities,
 *   sprints: sprintPlan,
 *   releases: releasePlan,
 * });
 * ```
 */
export function generateBatchPlan({ stories, dependencies, priorities, sprints, releases }) {
  // ── Input validation ──
  if (!stories || stories.length === 0) {
    return {
      batches: [],
      summary: { totalBatches: 0, devBatches: 0, testBatches: 0, releaseBatches: 0, deliveryBatches: 0 },
      warnings: ["No stories provided"],
    };
  }

  const warnings = [];
  const batches = [];

  const graph = dependencies && dependencies.graph ? dependencies.graph : (dependencies || { nodes: [], edges: [] });
  const storyMap = new Map();
  for (const s of stories) storyMap.set(s.id, s);

  // Build dependency map: storyId → set of dependency IDs
  const depMap = new Map();
  for (const s of stories) {
    depMap.set(s.id, new Set(s.dependencies || []));
  }
  for (const edge of (graph.edges || [])) {
    // edge.from depends on edge.to (SF-02 semantics: from REQUIRES to)
    if (!depMap.has(edge.from)) depMap.set(edge.from, new Set());
    depMap.get(edge.from).add(edge.to);
  }

  // ── Compute dependency levels ──
  const depLevels = new Map();
  function computeLevel(id, visited = new Set()) {
    if (visited.has(id)) return 0;
    visited.add(id);
    const deps = depMap.get(id) || new Set();
    if (deps.size === 0) {
      depLevels.set(id, 0);
      return 0;
    }
    let max = 0;
    for (const depId of deps) {
      max = Math.max(max, computeLevel(depId, visited) + 1);
    }
    depLevels.set(id, max);
    return max;
  }
  for (const s of stories) {
    if (!depLevels.has(s.id)) computeLevel(s.id);
  }

  // ── Priority map ──
  const priorityMap = new Map();
  for (const p of (priorities || [])) {
    if (p.itemType === "story") priorityMap.set(p.itemId, p.category);
  }

  // ── 1. Development Batches ──
  // Group by dependency level
  const levelGroups = new Map(); // level → [storyIds]
  for (const s of stories) {
    const level = depLevels.get(s.id) ?? 0;
    // Bucket: L0, L1, L2, L3+
    const bucket = level >= 3 ? "L3" : `L${level}`;
    if (!levelGroups.has(bucket)) levelGroups.set(bucket, []);
    levelGroups.get(bucket).push(s.id);
  }

  const levelOrder = ["L0", "L1", "L2", "L3"];
  for (const bucket of levelOrder) {
    const ids = levelGroups.get(bucket) || [];
    if (ids.length === 0) continue;

    // Stories at same level can be developed in parallel — group them
    const batchId = `dev-batch-${++_batchSeq}`;
    const batchName = `Development ${bucket.replace("L", "Level ")}`;

    // Collect feature and epic IDs
    const featureIds = new Set();
    const epicIds = new Set();
    for (const id of ids) {
      const s = storyMap.get(id);
      if (s) {
        if (s.featureId) featureIds.add(s.featureId);
        if (s.epicId) epicIds.add(s.epicId);
      }
    }

    // Prerequisite: all lower-level batches
    const levelNum = parseInt(bucket.slice(1));
    const prerequisiteBatches = [];
    for (let l = 0; l < levelNum; l++) {
      const lowerBucket = `L${l}`;
      const lowerIds = levelGroups.get(lowerBucket);
      if (lowerIds && lowerIds.length > 0) {
        prerequisiteBatches.push(`dev-batch-${lowerBucket}`);
      }
    }

    batches.push({
      batchId,
      batchType: "DevelopmentBatch",
      name: batchName,
      storyIds: ids,
      featureIds: [...featureIds],
      epicIds: [...epicIds],
      parallelGroups: groupForParallel(ids, depMap, depLevels, storyMap),
      prerequisiteBatches,
    });
  }

  // ── 2. Testing Batches ──
  // Group completed dev batches ready for testing (by sprint boundaries)
  if (sprints && sprints.sprints) {
    for (const sprint of sprints.sprints) {
      const testBatchId = `test-batch-${++_batchSeq}`;
      const batchName = `Testing Sprint ${sprint.sprintNumber}`;

      const featureIds = new Set();
      const epicIds = new Set();
      for (const sid of (sprint.storyIds || [])) {
        const s = storyMap.get(sid);
        if (s) {
          if (s.featureId) featureIds.add(s.featureId);
          if (s.epicId) epicIds.add(s.epicId);
        }
      }

      batches.push({
        batchId: testBatchId,
        batchType: "TestingBatch",
        name: batchName,
        storyIds: sprint.storyIds || [],
        featureIds: [...featureIds],
        epicIds: [...epicIds],
        parallelGroups: [sprint.storyIds || []], // All stories from a sprint can be tested together
        prerequisiteBatches: [`dev-batch-${batchName}`],
      });
    }
  }

  // ── 3. Release Batches ──
  // Group features ready for staged deployment (by release)
  if (releases && releases.releases) {
    for (const rel of releases.releases) {
      const releaseBatchId = `release-batch-${++_batchSeq}`;

      const featureIds = new Set();
      const epicIds = new Set();
      for (const sid of (rel.storyIds || [])) {
        const s = storyMap.get(sid);
        if (s) {
          if (s.featureId) featureIds.add(s.featureId);
          if (s.epicId) epicIds.add(s.epicId);
        }
      }

      batches.push({
        batchId: releaseBatchId,
        batchType: "ReleaseBatch",
        name: `Release ${rel.name} (${rel.version})`,
        storyIds: rel.storyIds || [],
        featureIds: [...featureIds],
        epicIds: [...epicIds],
        parallelGroups: [rel.storyIds || []],
        prerequisiteBatches: [],
      });
    }
  }

  // ── 4. Delivery Batches ──
  // Customer-facing delivery increments — group by release
  if (releases && releases.releases) {
    for (const rel of releases.releases) {
      const deliveryBatchId = `delivery-batch-${++_batchSeq}`;

      const featureIds = new Set();
      const epicIds = new Set();
      for (const sid of (rel.storyIds || [])) {
        const s = storyMap.get(sid);
        if (s) {
          if (s.featureId) featureIds.add(s.featureId);
          if (s.epicId) epicIds.add(s.epicId);
        }
      }

      batches.push({
        batchId: deliveryBatchId,
        batchType: "DeliveryBatch",
        name: `Delivery ${rel.name} (${rel.version})`,
        storyIds: rel.storyIds || [],
        featureIds: [...featureIds],
        epicIds: [...epicIds],
        parallelGroups: [],
        prerequisiteBatches: [`release-batch-${rel.name}`],
      });
    }
  }

  // ── Generate warnings ──
  if (levelGroups.size === 0) {
    warnings.push("No development batches generated — empty story list");
  }
  if (batches.length === 0) {
    warnings.push("No batches of any type could be generated");
  }

  const summary = {
    totalBatches: batches.length,
    devBatches: batches.filter(b => b.batchType === "DevelopmentBatch").length,
    testBatches: batches.filter(b => b.batchType === "TestingBatch").length,
    releaseBatches: batches.filter(b => b.batchType === "ReleaseBatch").length,
    deliveryBatches: batches.filter(b => b.batchType === "DeliveryBatch").length,
  };

  return { batches, summary, warnings };
}

/**
 * Group story IDs into parallel work groups based on dependency analysis.
 * Stories with no interdependency at the same level can be done in parallel.
 *
 * @param {string[]} ids — Story IDs at the same dependency level
 * @param {Map<string, Set<string>>} depMap — Dependency map
 * @param {Map<string, number>} depLevels — Computed levels
 * @param {Map<string, object>} storyMap — Story lookup
 * @returns {string[][]} Groups of stories that can be developed in parallel
 */
function groupForParallel(ids, depMap, depLevels, storyMap) {
  if (ids.length === 0) return [];
  if (ids.length === 1) return [ids];

  // Simple heuristic: group by epic ID so stories from same epic are together,
  // but different epics can proceed in parallel
  const epicGroups = new Map();
  for (const id of ids) {
    const s = storyMap.get(id);
    const epicId = s ? s.epicId || "unknown" : "unknown";
    if (!epicGroups.has(epicId)) epicGroups.set(epicId, []);
    epicGroups.get(epicId).push(id);
  }

  // Return epic-based groups as parallel units
  return [...epicGroups.values()];
}

export default { generateBatchPlan };
