#!/usr/bin/env -S uv run --script
#
# /// script
# dependencies = []
# [tool.uv]
# exclude-newer = "2023-10-16T00:00:00Z"
# ///
"""
deps - Todo.txt dependency management action

Supports both dep: and depends: syntax for task dependencies.
Provides blocking/blocked task views and dependency chain visualization.
"""

import sys
import re
import os
import uuid
from collections import defaultdict, deque

def usage():
    """Print usage information"""
    print("  deps - Task dependency management")
    print("    deps blocked                Show tasks that are blocked by dependencies")  
    print("    deps blocking               Show tasks that block other tasks")
    print("    deps ready                  Show tasks with no unmet dependencies")
    print("    deps chain TASK_ID          Show dependency chain for a task")
    print("    deps tree                   Show hierarchical view of all dependencies")
    print("    deps validate               Check for circular dependencies and orphaned refs")
    print("    deps addids                 Add UUID IDs to tasks that don't have them")
    print("")

def parse_task_line(line_num, line):
    """Parse a todo.txt line and extract task info"""
    line = line.strip()
    if not line or line.startswith('x '):  # Skip completed tasks
        return None
    
    task = {
        'line_num': line_num,
        'raw': line,
        'id': None,
        'deps': [],
        'sup': None
    }
    
    # Extract id: field
    id_match = re.search(r'\bid:([A-Za-z0-9_-]+)', line)
    if id_match:
        task['id'] = id_match.group(1)
    
    # Extract dep: and depends: fields  
    dep_matches = re.findall(r'\b(?:dep|depends):([A-Za-z0-9_,-]+)', line)
    for match in dep_matches:
        # Handle comma-separated lists
        deps = [d.strip() for d in match.split(',') if d.strip()]
        task['deps'].extend(deps)
    
    # Extract sup: field for hierarchies
    sup_match = re.search(r'\bsup:([A-Za-z0-9_-]+)', line)
    if sup_match:
        task['sup'] = sup_match.group(1)
    
    return task

def load_tasks():
    """Load and parse todo.txt file"""
    todo_file = os.environ.get('TODO_FILE')
    if not todo_file:
        print("Error: TODO_FILE environment variable not set")
        sys.exit(1)
    
    tasks = []
    task_ids = {}
    
    try:
        with open(todo_file, 'r') as f:
            for line_num, line in enumerate(f, 1):
                task = parse_task_line(line_num, line)
                if task:
                    tasks.append(task)
                    if task['id']:
                        if task['id'] in task_ids:
                            print(f"Warning: Duplicate ID '{task['id']}' on lines {task_ids[task['id']]} and {line_num}")
                        task_ids[task['id']] = line_num
    except FileNotFoundError:
        print(f"Error: Todo file {todo_file} not found")
        sys.exit(1)
    
    return tasks, task_ids

def find_circular_deps(tasks):
    """Find circular dependencies using DFS"""
    # Build adjacency list
    graph = defaultdict(list)
    for task in tasks:
        if task['id']:
            for dep in task['deps']:
                graph[dep].append(task['id'])
    
    visited = set()
    rec_stack = set()
    cycles = []
    
    def dfs(node, path):
        if node in rec_stack:
            # Found a cycle
            cycle_start = path.index(node)
            cycles.append(path[cycle_start:] + [node])
            return
        
        if node in visited:
            return
        
        visited.add(node)
        rec_stack.add(node)
        
        for neighbor in graph[node]:
            dfs(neighbor, path + [neighbor])
        
        rec_stack.remove(node)
    
    for task in tasks:
        if task['id'] and task['id'] not in visited:
            dfs(task['id'], [task['id']])
    
    return cycles

def show_blocked_tasks(tasks, task_ids):
    """Show tasks that are blocked by dependencies"""
    blocked = []
    
    for task in tasks:
        if task['deps']:
            unmet_deps = []
            for dep_id in task['deps']:
                if dep_id not in task_ids:
                    unmet_deps.append(f"{dep_id} (missing)")
                else:
                    # For now, assume all existing deps are unmet
                    # In a full implementation, we'd check completion status
                    unmet_deps.append(dep_id)
            
            if unmet_deps:
                blocked.append((task, unmet_deps))
    
    if not blocked:
        print("No blocked tasks found")
        return
    
    print("Blocked tasks:")
    for task, deps in blocked:
        print(f"{task['line_num']:2d} {task['raw']}")
        print(f"   Blocked by: {', '.join(deps)}")

def show_blocking_tasks(tasks, task_ids):
    """Show tasks that block other tasks"""
    blocking = defaultdict(list)
    
    # Find what each task blocks
    for task in tasks:
        for dep_id in task['deps']:
            if dep_id in task_ids:
                blocking[dep_id].append(task)
    
    if not blocking:
        print("No blocking tasks found")
        return
    
    print("Blocking tasks:")
    for task in tasks:
        if task['id'] and task['id'] in blocking:
            print(f"{task['line_num']:2d} {task['raw']}")
            blocked_tasks = blocking[task['id']]
            for blocked in blocked_tasks:
                print(f"   Blocks: {blocked['line_num']} {blocked['raw'][:60]}...")

def show_ready_tasks(tasks, task_ids):
    """Show tasks with no unmet dependencies"""
    ready = []
    
    for task in tasks:
        if not task['deps']:
            ready.append(task)
        else:
            # Check if all deps are met (for now, just check if they exist)
            all_deps_exist = all(dep_id in task_ids for dep_id in task['deps'])
            if all_deps_exist:
                # In full implementation, would check completion status
                pass
            else:
                ready.append(task)  # Missing deps = treat as ready for now
    
    if not ready:
        print("No ready tasks found")
        return
    
    print("Ready tasks (no unmet dependencies):")
    for task in ready:
        print(f"{task['line_num']:2d} {task['raw']}")

def validate_dependencies(tasks, task_ids):
    """Validate dependencies for issues"""
    issues = []
    
    # Check for orphaned references
    for task in tasks:
        for dep_id in task['deps']:
            if dep_id not in task_ids:
                issues.append(f"Line {task['line_num']}: depends on missing task '{dep_id}'")
        
        if task['sup'] and task['sup'] not in task_ids:
            issues.append(f"Line {task['line_num']}: parent task '{task['sup']}' not found")
    
    # Check for circular dependencies
    cycles = find_circular_deps(tasks)
    for cycle in cycles:
        cycle_str = " → ".join(cycle)
        issues.append(f"Circular dependency: {cycle_str}")
    
    if not issues:
        print("No dependency issues found")
    else:
        print("Dependency validation issues:")
        for issue in issues:
            print(f"  {issue}")

def generate_short_uuid():
    """Generate a short, readable UUID (8 chars)"""
    return str(uuid.uuid4())[:8]

def trace_dependency_chain(target_id, tasks, task_ids):
    """Trace what blocks a specific task"""
    if target_id not in task_ids:
        print(f"Task ID '{target_id}' not found")
        return
    
    # Find the target task
    target_task = None
    for task in tasks:
        if task['id'] == target_id:
            target_task = task
            break
    
    if not target_task:
        print(f"Task with ID '{target_id}' not found")
        return
    
    print(f"Dependency chain for '{target_id}':")
    print(f"  {target_task['line_num']:2d} {target_task['raw']}")
    
    if not target_task['deps']:
        print("  └─ No dependencies (ready to start)")
        return
    
    # Show direct dependencies
    visited = {target_id}  # Start with target to prevent cycles back to it
    for dep_id in target_task['deps']:
        if dep_id in task_ids:
            dep_task = next(t for t in tasks if t['id'] == dep_id)
            print(f"  └─ Depends on: {dep_task['line_num']:2d} {dep_task['raw'][:60]}...")
            
            # Recursively show dependencies of dependencies
            _show_dep_chain_recursive(dep_id, tasks, task_ids, "    ", visited.copy())
        else:
            print(f"  └─ Depends on: {dep_id} (MISSING!)")

def _show_dep_chain_recursive(task_id, tasks, task_ids, indent, visited=None):
    """Recursively show dependency chain with cycle detection"""
    if visited is None:
        visited = set()
    
    if task_id in visited:
        print(f"{indent}└─ {task_id} (CIRCULAR DEPENDENCY!)")
        return
    
    visited.add(task_id)
    
    task = next((t for t in tasks if t['id'] == task_id), None)
    if not task or not task['deps']:
        visited.remove(task_id)
        return
    
    for dep_id in task['deps']:
        if dep_id in task_ids:
            dep_task = next(t for t in tasks if t['id'] == dep_id)
            print(f"{indent}└─ {dep_task['line_num']:2d} {dep_task['raw'][:50]}...")
            _show_dep_chain_recursive(dep_id, tasks, task_ids, indent + "  ", visited)
        else:
            print(f"{indent}└─ {dep_id} (MISSING!)")
    
    visited.remove(task_id)

def show_dependency_tree(tasks, task_ids):
    """Show hierarchical tree view of all dependencies"""
    # Find root tasks (tasks with no dependencies or missing dependencies)
    roots = []
    for task in tasks:
        if not task['deps'] or not all(dep in task_ids for dep in task['deps']):
            roots.append(task)
    
    if not roots:
        print("No root tasks found (all tasks have dependencies)")
        return
    
    print("Dependency tree:")
    print("")
    
    # Show each root and its dependents
    for root in roots:
        if root['id']:
            print(f"📋 {root['line_num']:2d} {root['raw']}")
            _show_dependents_recursive(root['id'], tasks, task_ids, "  ")
        else:
            print(f"📋 {root['line_num']:2d} {root['raw']} (no ID)")
        print("")

def _show_dependents_recursive(task_id, tasks, task_ids, indent, visited=None, depth=0):
    """Recursively show what depends on a task with cycle detection"""
    if visited is None:
        visited = set()
    
    # Prevent infinite recursion
    if task_id in visited or depth > 10:
        if depth > 10:
            print(f"{indent}└─ ♾️ (max depth reached)")
        return
    
    visited.add(task_id)
    
    dependents = []
    for task in tasks:
        if task_id in task['deps']:
            dependents.append(task)
    
    for i, dep_task in enumerate(dependents):
        is_last = i == len(dependents) - 1
        connector = "└─" if is_last else "├─"
        status = "⏳" if dep_task['deps'] else "✅"
        
        if dep_task['id'] in visited:
            print(f"{indent}{connector} {status} {dep_task['line_num']:2d} {dep_task['raw'][:60]}... (CIRCULAR)")
        else:
            print(f"{indent}{connector} {status} {dep_task['line_num']:2d} {dep_task['raw'][:60]}...")
            
            if dep_task['id']:
                next_indent = indent + ("   " if is_last else "│  ")
                _show_dependents_recursive(dep_task['id'], tasks, task_ids, next_indent, visited.copy(), depth + 1)
    
    visited.remove(task_id)

def add_missing_ids():
    """Add IDs to tasks that don't have them"""
    todo_file = os.environ.get('TODO_FILE')
    if not todo_file:
        print("Error: TODO_FILE environment variable not set")
        return 1
    
    try:
        with open(todo_file, 'r') as f:
            lines = f.readlines()
    except FileNotFoundError:
        print(f"Error: Todo file {todo_file} not found")
        return 1
    
    modified = False
    existing_ids = set()
    
    # First pass: collect existing IDs
    for line in lines:
        id_match = re.search(r'\bid:([A-Za-z0-9_-]+)', line)
        if id_match:
            existing_ids.add(id_match.group(1))
    
    # Second pass: add IDs where missing
    for i, line in enumerate(lines):
        line = line.strip()
        if not line or line.startswith('x '):  # Skip completed tasks
            continue
        
        # Check if task already has an ID
        if re.search(r'\bid:[A-Za-z0-9_-]+', line):
            continue
            
        # Generate unique ID
        new_id = generate_short_uuid()
        while new_id in existing_ids:
            new_id = generate_short_uuid()
        existing_ids.add(new_id)
        
        # Add ID to end of line
        lines[i] = f"{line} id:{new_id}\n"
        modified = True
        print(f"Added id:{new_id} to line {i+1}: {line[:50]}...")
    
    if modified:
        # Write back to file
        with open(todo_file, 'w') as f:
            f.writelines(lines)
        print(f"Updated {todo_file} with new IDs")
    else:
        print("No tasks needed ID assignment")
    
    return 0

def main():
    if len(sys.argv) < 2:
        usage()
        return 1
    
    action = sys.argv[1]
    
    if action == "usage":
        usage()
        return 0
    
    tasks, task_ids = load_tasks()
    
    if action == "blocked":
        show_blocked_tasks(tasks, task_ids)
    elif action == "blocking": 
        show_blocking_tasks(tasks, task_ids)
    elif action == "ready":
        show_ready_tasks(tasks, task_ids)
    elif action == "validate":
        validate_dependencies(tasks, task_ids)
    elif action == "addids":
        return add_missing_ids()
    elif action == "chain":
        if len(sys.argv) < 3:
            print("Usage: deps chain TASK_ID")
            return 1
        target_id = sys.argv[2]
        trace_dependency_chain(target_id, tasks, task_ids)
    elif action == "tree":
        show_dependency_tree(tasks, task_ids)
    else:
        print(f"Unknown action: {action}")
        usage()
        return 1
    
    return 0

if __name__ == "__main__":
    sys.exit(main())
