import csv
import math
from queue import Queue


def build_actor_graph(csv_filepath):
    """
    Reads the .csv file and builds an adjacency list.
    Graph structure: { actor_name: set([co_star1, co_star2, ...]) }
    """
    graph = {}

    with open(
        csv_filepath, mode='r', encoding='utf-8', errors='ignore'
    ) as csv_file:
        # Using DictReader to access columns by their header names
        reader = csv.DictReader(csv_file)

        for row in reader:
            # Extract the top 3 actors, stripping whitespace
            a1 = row.get('actor_1_name', '').strip()
            a2 = row.get('actor_2_name', '').strip()
            a3 = row.get('actor_3_name', '').strip()

            # Put them in a list and filter out any empty strings/missing data
            actors = [a for a in [a1, a2, a3] if a]

            # add them to the graph
            for actor in actors:
                if actor not in graph:
                    graph[actor] = set()

            # Connect every actor in this movie to every other actor in this
            # movie
            for i in range(len(actors)):
                for j in range(i + 1, len(actors)):
                    actor_a = actors[i]
                    actor_b = actors[j]

                    graph[actor_a].add(actor_b)
                    graph[actor_b].add(actor_a)

    return graph


def breadth_first_search(graph, start_actor, marked=None):
    """
    Performs a BFS on the graph given as an adjacency list following the
    classical BFS algorithm. Explores all nodes reachable from the start_actor.
    Returns a dictionary of distances mapping each actor in the graph to their
    distance (level) from start_actor, where unreachable actors have a distance
    of math.inf.

    >>> dummy_graph = {
    ...     'A': ['B', 'C'],
    ...     'B': ['A', 'C'],
    ...     'C': ['A', 'B'],
    ...     'D': ['E'],
    ...     'E': ['D'],
    ...     'F': []
    ... }
    >>> dists = breadth_first_search(dummy_graph, 'A')
    >>> dists['A']
    0
    >>> dists['B']
    1
    >>> dists['D']
    inf
    """
    # Classical initialization: all nodes are unmarked and distances are set
    # to infinity
    if marked is None:
        marked = {actor: False for actor in graph}
    distances = {actor: math.inf for actor in graph}

    if start_actor not in graph:
        return distances

    marked[start_actor] = True
    distances[start_actor] = 0

    # Queue stores actors to explore
    queue = Queue()
    queue.put(start_actor)

    while not queue.empty():
        u = queue.get()

        for v in graph[u]:
            if not marked[v]:
                marked[v] = True
                distances[v] = distances[u] + 1
                queue.put(v)

    return distances


def compute_connected_components(graph):
    """
    Computes all connected components of the graph.
    Returns a list of nodes, where each node is a representative of its
    connected component.
    For the Bonus Task: Also store the size of each component.
    So the function will return a list of tuples of the form
    (representative, size).

    >>> dummy_graph = {
    ...     'A': ['B', 'C'],
    ...     'B': ['A', 'C'],
    ...     'C': ['A', 'B'],
    ...     'D': ['E'],
    ...     'E': ['D'],
    ...     'F': []
    ... }
    >>> compute_connected_components(dummy_graph)
    [('A', 3), ('D', 2), ('F', 1)]
    """
    marked = {actor: False for actor in graph}
    components = []
    number_of_marked = 0
    for u in graph:
        if not marked[u]:
            breadth_first_search(graph, u, marked)
            current_marked_count = sum(1 for val in marked.values() if val)
            components.append((u, current_marked_count - number_of_marked))
            number_of_marked = current_marked_count

    components.sort(key=lambda x: x[1], reverse=True)
    return components


# ==========================================
# Example Usage:
# ==========================================
if __name__ == "__main__":

    csv_file_path = "movies.csv"
    print("Loading data and building graph...")

    try:
        actor_graph = build_actor_graph(csv_file_path)
        print(
            f"Graph loaded successfully! Total unique actors "
            f"found: {len(actor_graph)}"
        )

        # Define your start and target actors
        start = "Tom Hanks"
        target = "Orlando Bloom"

        print(
            f"\nSearching for the distance between '{start}' and "
            f"'{target}'..."
        )
        distances = breadth_first_search(actor_graph, start)

        distance_to_target = distances.get(target)

        if distance_to_target is not None and distance_to_target != math.inf:
            print(f"Success! Found a path with distance {distance_to_target}.")
        else:
            print("No connection could be found between these two actors.")


        print("Compute Connected components....")
        comp = compute_connected_components(actor_graph)
        print(f"Found {len(comp)} many components.")
        print(
            f"Largest component is the component of {comp[0][0]} with "
            f"{comp[0][1]} actors."
        )
        print(
            f"Second largest component is the component of {comp[1][0]} "
            f"with {comp[1][1]} actors."
        )
        print(
            f"The smallest component is the component of {comp[-1][0]} "
            f"with {comp[-1][1]} actors."
        )

    except FileNotFoundError:
        print(
            f"Error: Could not find '{csv_file_path}'. Please make sure "
            f"the file is in the same directory."
        )
