import os
import yaml
import argparse
from collections import defaultdict, deque

def find_roles(roles_dir, prefix=None):
    """Find all roles in the given directory."""
    for entry in os.listdir(roles_dir):
        if prefix and not entry.startswith(prefix):
            continue
        path = os.path.join(roles_dir, entry)
        meta_file = os.path.join(path, 'meta', 'main.yml')
        if os.path.isdir(path) and os.path.isfile(meta_file):
            yield path, meta_file

def load_run_after(meta_file):
    """Load the 'run_after' from the meta/main.yml of a role."""
    with open(meta_file, 'r') as f:
        data = yaml.safe_load(f) or {}
    return data.get('galaxy_info', {}).get('run_after', [])

def load_application_id(role_path):
    """Load the application_id from the vars/main.yml of the role."""
    vars_file = os.path.join(role_path, 'vars', 'main.yml')
    if os.path.exists(vars_file):
        with open(vars_file, 'r') as f:
            data = yaml.safe_load(f) or {}
        return data.get('application_id')
    return None

def build_dependency_graph(roles_dir, prefix=None):
    """Build a dependency graph where each role points to the roles it depends on."""
    graph = defaultdict(list)
    in_degree = defaultdict(int)
    roles = {}

    for role_path, meta_file in find_roles(roles_dir, prefix):
        run_after = load_run_after(meta_file)
        application_id = load_application_id(role_path)
        role_name = os.path.basename(role_path)
        roles[role_name] = {
            'role_name': role_name,
            'run_after': run_after,
            'application_id': application_id,
            'path': role_path
        }

        # If the role has dependencies, build the graph
        for dependency in run_after:
            graph[dependency].append(role_name)
            in_degree[role_name] += 1

        # Ensure roles with no dependencies have an in-degree of 0
        if role_name not in in_degree:
            in_degree[role_name] = 0

    return graph, in_degree, roles

def topological_sort(graph, in_degree):
    """Perform topological sort on the dependency graph."""
    # Queue for roles with no incoming dependencies (in_degree == 0)
    queue = deque([role for role, degree in in_degree.items() if degree == 0])
    sorted_roles = []

    while queue:
        role = queue.popleft()
        sorted_roles.append(role)

        # Reduce in-degree for roles dependent on the current role
        for neighbor in graph[role]:
            in_degree[neighbor] -= 1
            if in_degree[neighbor] == 0:
                queue.append(neighbor)

    if len(sorted_roles) != len(in_degree):
        # If the number of sorted roles doesn't match the number of roles,
        # there was a cycle in the graph (not all roles could be sorted)
        raise Exception("Circular dependency detected among the roles!")

    return sorted_roles

def print_dependency_tree(graph):
    """Print the dependency tree visually on the console."""
    def print_node(role, indent=0):
        print("  " * indent + role)
        for dependency in graph[role]:
            print_node(dependency, indent + 1)

    # Print the tree starting from roles with no dependencies
    all_roles = set(graph.keys())
    dependent_roles = {role for dependencies in graph.values() for role in dependencies}
    root_roles = all_roles - dependent_roles

    printed_roles = []

    def collect_roles(role, indent=0):
        printed_roles.append(role)
        for dependency in graph[role]:
            collect_roles(dependency, indent + 1)

    for root in root_roles:
        collect_roles(root)

    return printed_roles

def generate_playbook_entries(roles_dir, prefix=None):
    """Generate playbook entries based on the sorted order."""
    # Build dependency graph
    graph, in_degree, roles = build_dependency_graph(roles_dir, prefix)

    # Print and collect roles in tree order
    tree_sorted_roles = print_dependency_tree(graph)

    # Topologically sort the roles
    sorted_role_names = topological_sort(graph, in_degree)

    # Ensure that roles that appear in the tree come first
    final_sorted_roles = [role for role in tree_sorted_roles if role in sorted_role_names]

    # Include the remaining unsorted roles
    final_sorted_roles += [role for role in sorted_role_names if role not in final_sorted_roles]

    # Remove duplicates, keeping only the first occurrence to preserve dependency order
    seen = set()
    deduplicated_roles = []
    for role in final_sorted_roles:
        if role not in seen:
            deduplicated_roles.append(role)
            seen.add(role)

    # Generate the playbook entries
    entries = []
    for role_name in deduplicated_roles:
        role = roles[role_name]
        entries.append(
            f"- name: setup {role['application_id']}\n"
            f"  when: ('{role['application_id']}' in group_names)\n"
            f"  include_role:\n"
            f"    name: {role['role_name']}\n"
        )
        entries.append(
            f"- name: flush handlers after {role['application_id']}\n"
            f"  meta: flush_handlers\n"
        )

    return entries

def main():
    parser = argparse.ArgumentParser(
        description='Generate an Ansible playbook include file from Docker roles, sorted by run_after order.'
    )
    parser.add_argument(
        'roles_dir',
        help='Path to directory containing role folders'
    )
    parser.add_argument(
        '-p', '--prefix',
        help='Only include roles whose names start with this prefix (e.g. docker-, client-)',
        default=None
    )
    parser.add_argument(
        '-o', '--output',
        help='Output file path (default: stdout)',
        default=None
    )
    parser.add_argument(
        '-t', '--tree',
        action='store_true',
        help='Display the dependency tree of roles visually'
    )
    args = parser.parse_args()

    # Generate and output the playbook entries
    entries = generate_playbook_entries(args.roles_dir, args.prefix)
    output = ''.join(entries)

    if args.output:
        with open(args.output, 'w') as f:
            f.write(output)
        print(f"Playbook entries written to {args.output}")
    else:
        print(output)

if __name__ == '__main__':
    main()
