"""Procedural phone book data structures for assignment 1.

The task explicitly asks to avoid classes, so every structure is represented
with plain dictionaries, lists and functions.
"""


def _make_ll_node(name, phone, next_node=None):
    return {"name": name, "phone": phone, "next": next_node}


def ll_insert(head, name, phone):
    """Insert or update a record in a linked list, returning the head."""
    if head is None:
        return _make_ll_node(name, phone)

    current = head
    while current is not None:
        if current["name"] == name:
            current["phone"] = phone
            return head
        if current["next"] is None:
            break
        current = current["next"]

    current["next"] = _make_ll_node(name, phone)
    return head


def ll_find(head, name):
    """Return a phone by name or None if there is no such record."""
    current = head
    while current is not None:
        if current["name"] == name:
            return current["phone"]
        current = current["next"]
    return None


def ll_delete(head, name):
    """Delete a record by name, returning the possibly changed head."""
    previous = None
    current = head

    while current is not None:
        if current["name"] == name:
            if previous is None:
                return current["next"]
            previous["next"] = current["next"]
            return head

        previous = current
        current = current["next"]

    return head


def ll_list_all(head):
    """Return all linked-list records sorted by name."""
    records = []
    current = head
    while current is not None:
        records.append((current["name"], current["phone"]))
        current = current["next"]
    return sorted(records, key=lambda item: item[0])


def create_hash_table(size=20011):
    """Create a fixed-size hash table with separate chaining."""
    return [None for _ in range(size)]


def _hash_name(name, bucket_count):
    """Stable polynomial hash, unlike Python's randomized built-in hash()."""
    value = 0
    for char in name:
        value = (value * 31 + ord(char)) % bucket_count
    return value


def ht_insert(buckets, name, phone):
    """Insert or update a record in the hash table."""
    index = _hash_name(name, len(buckets))
    buckets[index] = ll_insert(buckets[index], name, phone)


def ht_find(buckets, name):
    """Return a phone by name or None if there is no such record."""
    index = _hash_name(name, len(buckets))
    return ll_find(buckets[index], name)


def ht_delete(buckets, name):
    """Delete a record by name if it exists."""
    index = _hash_name(name, len(buckets))
    buckets[index] = ll_delete(buckets[index], name)


def ht_list_all(buckets):
    """Return all hash-table records sorted by name."""
    records = []
    for head in buckets:
        current = head
        while current is not None:
            records.append((current["name"], current["phone"]))
            current = current["next"]
    return sorted(records, key=lambda item: item[0])


def _make_bst_node(name, phone):
    return {"name": name, "phone": phone, "left": None, "right": None}


def bst_insert(root, name, phone):
    """Insert or update a record in a binary search tree."""
    if root is None:
        return _make_bst_node(name, phone)

    current = root
    while True:
        if name == current["name"]:
            current["phone"] = phone
            return root

        if name < current["name"]:
            if current["left"] is None:
                current["left"] = _make_bst_node(name, phone)
                return root
            current = current["left"]
        else:
            if current["right"] is None:
                current["right"] = _make_bst_node(name, phone)
                return root
            current = current["right"]


def bst_find(root, name):
    """Return a phone by name or None if there is no such record."""
    current = root
    while current is not None:
        if name == current["name"]:
            return current["phone"]
        if name < current["name"]:
            current = current["left"]
        else:
            current = current["right"]
    return None


def _detach_min(node):
    """Detach the minimal node from a subtree and return (new_subtree, min)."""
    parent = None
    current = node

    while current["left"] is not None:
        parent = current
        current = current["left"]

    if parent is None:
        return current["right"], current

    parent["left"] = current["right"]
    current["right"] = None
    return node, current


def bst_delete(root, name):
    """Delete a record from the tree, returning the possibly changed root."""
    parent = None
    current = root

    while current is not None and current["name"] != name:
        parent = current
        if name < current["name"]:
            current = current["left"]
        else:
            current = current["right"]

    if current is None:
        return root

    if current["left"] is None:
        replacement = current["right"]
    elif current["right"] is None:
        replacement = current["left"]
    else:
        new_right, successor = _detach_min(current["right"])
        successor["left"] = current["left"]
        successor["right"] = new_right
        replacement = successor

    if parent is None:
        return replacement

    if parent["left"] is current:
        parent["left"] = replacement
    else:
        parent["right"] = replacement

    return root


def bst_list_all(root):
    """Return all BST records sorted by name using in-order traversal."""
    records = []
    stack = []
    current = root

    while current is not None or stack:
        while current is not None:
            stack.append(current)
            current = current["left"]

        current = stack.pop()
        records.append((current["name"], current["phone"]))
        current = current["right"]

    return records


def _assert_basic_operations():
    records = [("Boris", "222"), ("Anna", "111"), ("Denis", "444")]
    expected_sorted = [("Anna", "111"), ("Boris", "222"), ("Denis", "444")]

    head = None
    for name, phone in records:
        head = ll_insert(head, name, phone)
    assert ll_find(head, "Anna") == "111"
    head = ll_insert(head, "Anna", "333")
    assert ll_find(head, "Anna") == "333"
    head = ll_delete(head, "Anna")
    assert ll_find(head, "Anna") is None
    assert ll_list_all(head) == [("Boris", "222"), ("Denis", "444")]

    table = create_hash_table(17)
    for name, phone in records:
        ht_insert(table, name, phone)
    assert ht_find(table, "Denis") == "444"
    ht_insert(table, "Denis", "555")
    assert ht_find(table, "Denis") == "555"
    ht_delete(table, "Missing")
    assert ("Anna", "111") in ht_list_all(table)

    root = None
    for name, phone in records:
        root = bst_insert(root, name, phone)
    assert bst_list_all(root) == expected_sorted
    root = bst_delete(root, "Boris")
    assert bst_find(root, "Boris") is None
    assert bst_list_all(root) == [("Anna", "111"), ("Denis", "444")]


if __name__ == "__main__":
    _assert_basic_operations()
    print("All phonebook checks passed.")
