#!/usr/bin/env bash
# pi-epic-next-feature
#
# Prints the next ready feature ID for the active epic, or:
#   "DONE"                     all features merged
#   "HALT:<reason>"            something needs human attention
#
# A feature is ready iff:
#   - state in {pending}
#   - all depends_on are state=merged
#
# Picks the lowest ID among ready features (deterministic).
#
# v0.8.0 / L-048+L-049 — batch mode for parallel dispatcher:
#   --batch N    print up to N ready features (one per line) instead of
#                just the next one. Applies a hard conflict pre-check:
#                two features whose declared scope_files overlap are
#                NEVER returned in the same batch. The pre-check is the
#                mechanical enforcement of "no parallel dispatch of
#                features with overlapping declared scope" (L-049). The
#                in-process orchestrator queue handles the actual
#                concurrency in /epic-run-auto.md.

set -euo pipefail
# Resolve script dir through symlinks so we can source siblings reliably.
__src="${BASH_SOURCE[0]}"
while [ -L "$__src" ]; do
    __dir="$(cd -P "$(dirname "$__src")" && pwd)"
    __src="$(readlink "$__src")"
    [[ $__src != /* ]] && __src="$__dir/$__src"
done
__SCRIPT_DIR="$(cd -P "$(dirname "$__src")" && pwd)"
source "$__SCRIPT_DIR/_common.sh"

BATCH_N=1
while [[ $# -gt 0 ]]; do
    case "$1" in
        --batch)
            BATCH_N="${2:-}"
            [[ "$BATCH_N" =~ ^[0-9]+$ && $BATCH_N -ge 1 ]] || {
                echo "ERROR: --batch requires a positive integer (got: '$BATCH_N')" >&2; exit 2;
            }
            shift 2;;
        --help|-h)
            cat <<EOF
Usage: pi-epic-next-feature [--batch N]

  --batch N   Print up to N ready feature IDs (one per line) instead of
              the single next one. Features whose declared scope_files
              overlap with another feature already in the batch are
              skipped — the pre-check ensures parallel dispatch never
              sends two workers at the same shared file (L-049).
EOF
            exit 0;;
        *) echo "ERROR: unknown flag: $1" >&2; exit 2;;
    esac
done

epic_dir=$(active_epic_dir)
decomp="$epic_dir/decomposition.yaml"
features_dir="$epic_dir/features"
done_dir="$features_dir/done"
[[ -f "$decomp" ]] || { echo "HALT:no-decomposition"; exit 0; }

mkdir -p "$features_dir" "$done_dir"

python3 - "$decomp" "$features_dir" "$done_dir" "$BATCH_N" <<'PY'
import sys, os, re, glob

decomp_path, feats_dir, done_dir = sys.argv[1], sys.argv[2], sys.argv[3]
batch_n = int(sys.argv[4])

# Reuse the parser logic — minimal duplication
def parse(p):
    out = {'features': []}
    cur = None
    cur_list = None
    with open(p, encoding='utf-8') as f:
        for raw in f:
            line = raw.rstrip('\n')
            if not line.strip() or line.lstrip().startswith('#'):
                continue
            ind = len(line) - len(line.lstrip(' '))
            s = line.strip()
            if ind == 0 and re.match(r'^features\s*:', s):
                continue
            if s.startswith('- ') and 'id:' in s:
                cur = {}
                out['features'].append(cur)
                m = re.match(r'^id\s*:\s*(.*)$', s[2:])
                if m: cur['id'] = m.group(1).strip().strip('"').strip("'")
                cur_list = None
                continue
            if cur is None: continue
            if s.startswith('- '):
                if cur_list is not None:
                    cur_list.append(s[2:].strip().strip('"').strip("'"))
                continue
            m = re.match(r'^([A-Za-z0-9_]+)\s*:\s*(.*)$', s)
            if not m: continue
            k, v = m.group(1), re.sub(r'\s+#.*$', '', m.group(2).strip()).strip()
            if v == '':
                cur[k] = []; cur_list = cur[k]
            elif v.startswith('[') and v.endswith(']'):
                inner = v[1:-1].strip()
                cur[k] = [x.strip().strip('"').strip("'") for x in inner.split(',')] if inner else []
                cur_list = None
            else:
                cur[k] = v.strip('"').strip("'"); cur_list = None
    return out

# Read feature state from feature meta.yaml (in features/F.../ or features/done/F.../)
def state_of(fid):
    for d in (feats_dir, done_dir):
        for sub in os.listdir(d) if os.path.isdir(d) else []:
            if sub.startswith(fid):
                meta = os.path.join(d, sub, 'meta.yaml')
                if os.path.isfile(meta):
                    with open(meta) as f:
                        for line in f:
                            m = re.match(r'^state\s*:\s*(\S+)', line.strip())
                            if m: return m.group(1).strip().strip('"').strip("'")
    return 'pending'  # not yet started → pending

data = parse(decomp_path)
features = data.get('features', [])
if not features:
    print("HALT:no-features-in-decomposition"); sys.exit(0)

states = {ft['id']: state_of(ft['id']) for ft in features}

# Halted feature blocks progress
halted = [fid for fid, st in states.items() if st == 'halted']
if halted:
    print(f"HALT:feature-halted:{halted[0]}"); sys.exit(0)

# All merged? → DONE
if all(st == 'merged' for st in states.values()):
    print("DONE"); sys.exit(0)

# Resuming-first: if any feature is already in-progress (orchestrator was
# halted/restarted mid-feature), resume it before opening a NEW worktree.
# Otherwise we'd leak the partial worktree and risk DAG drift.
in_progress = [fid for fid, st in states.items() if st == 'in-progress']
if in_progress:
    # Pick the lowest-numbered in-progress one for determinism.
    print(sorted(in_progress)[0]); sys.exit(0)

# Find ready: pending and all deps merged
def is_ready(ft):
    if states[ft['id']] != 'pending': return False
    for d in ft.get('depends_on') or []:
        if states.get(d) != 'merged': return False
    return True

ready = sorted([ft['id'] for ft in features if is_ready(ft)])
if not ready:
    print("HALT:no-ready-feature-and-none-in-progress"); sys.exit(0)

if batch_n == 1:
    print(ready[0]); sys.exit(0)

# v0.8.0 batch mode: apply hard conflict pre-check.
# Build fid → scope_files map; greedily admit features into the batch
# only when their scope is disjoint from every already-admitted member.
scope = {}
for ft in features:
    sf = ft.get('scope_files') or []
    scope[ft['id']] = set(s for s in sf if s)

admitted = []
for fid in ready:
    if len(admitted) >= batch_n:
        break
    overlap = False
    for already in admitted:
        if scope[fid] & scope[already]:
            overlap = True
            break
    if not overlap:
        admitted.append(fid)

for fid in admitted:
    print(fid)
PY
