from __future__ import annotations

import csv
import random
import time
from pathlib import Path
from typing import Dict, List, Tuple

from linked_list import ll_insert, ll_find, ll_delete
from hash_table import ht_insert, ht_find, ht_delete
from bst import bst_insert, bst_find, bst_delete
from utils import generate_records, prepare_records_variants


Record = Tuple[str, str]


def make_missing_names(count: int = 10) -> List[str]:
    return [f"None_{i}" for i in range(count)]


def pick_existing_names(records: List[Record], count: int, seed: int = 42) -> List[str]:
    rng = random.Random(seed)
    unique_names = list(dict.fromkeys(name for name, _ in records))
    if len(unique_names) < count:
        raise ValueError(f"Not enough unique names: need {count}, got {len(unique_names)}")
    return rng.sample(unique_names, count)


def pick_delete_names(records: List[Record], count: int = 50, seed: int = 43) -> List[str]:
    rng = random.Random(seed)
    unique_names = list(dict.fromkeys(name for name, _ in records))
    if len(unique_names) < count:
        raise ValueError(f"Not enough unique names: need {count}, got {len(unique_names)}")
    return rng.sample(unique_names, count)


def build_structure(structure_name: str, records: List[Record], buckets_count: int = 2048):
    if structure_name == "linked_list":
        structure = None
        for name, phone in records:
            structure = ll_insert(structure, name, phone)
        return structure

    if structure_name == "hash_table":
        buckets = [None] * buckets_count
        for name, phone in records:
            buckets = ht_insert(buckets, name, phone)
        return buckets

    if structure_name == "bst":
        root = None
        for name, phone in records:
            root = bst_insert(root, name, phone)
        return root

    raise ValueError(f"Unknown structure: {structure_name}")


def do_find(structure_name: str, structure: object, existing_names: List[str], missing_names: List[str]) -> None:
    if structure_name == "linked_list":
        for name in existing_names:
            ll_find(structure, name)
        for name in missing_names:
            ll_find(structure, name)
        return

    if structure_name == "hash_table":
        for name in existing_names:
            ht_find(structure, name)
        for name in missing_names:
            ht_find(structure, name)
        return

    if structure_name == "bst":
        for name in existing_names:
            bst_find(structure, name)
        for name in missing_names:
            bst_find(structure, name)
        return

    raise ValueError(f"Unknown structure: {structure_name}")


def do_delete(structure_name: str, structure: object, delete_names: List[str]):
    if structure_name == "linked_list":
        for name in delete_names:
            structure = ll_delete(structure, name)
        return structure

    if structure_name == "hash_table":
        for name in delete_names:
            structure = ht_delete(structure, name)
        return structure

    if structure_name == "bst":
        for name in delete_names:
            structure = bst_delete(structure, name)
        return structure

    raise ValueError(f"Unknown structure: {structure_name}")


def measure_once(structure_name: str, records: List[Record], buckets_count: int = 2048) -> Dict[str, float]:
    existing_names = pick_existing_names(records, 100, seed=42)
    missing_names = make_missing_names(10)
    delete_names = pick_delete_names(records, 50, seed=43)

    start = time.perf_counter()
    structure = build_structure(structure_name, records, buckets_count=buckets_count)
    insert_time = time.perf_counter() - start

    start = time.perf_counter()
    do_find(structure_name, structure, existing_names, missing_names)
    find_time = time.perf_counter() - start

    start = time.perf_counter()
    structure = do_delete(structure_name, structure, delete_names)
    delete_time = time.perf_counter() - start

    return {"insert": insert_time, "find": find_time, "delete": delete_time}


def run_experiments(n: int = 10000, buckets_count: int = 2048, repeats: int = 5):
    records = generate_records(n, repeat_names=False)
    records_shuffled, records_sorted = prepare_records_variants(records)

    datasets = [
        ("случайный", records_shuffled),
        ("отсортированный", records_sorted),
    ]
    structures = [
        ("LinkedList", "linked_list"),
        ("HashTable", "hash_table"),
        ("BST", "bst"),
    ]
    operations = ("insert", "find", "delete")

    rows = [["Структура", "Режим", "Операция", "Замер", "Время (сек)"]]

    for mode_name, dataset_records in datasets:
        for human_name, structure_name in structures:
            times_by_op = {op: [] for op in operations}

            for attempt in range(1, repeats + 1):
                result = measure_once(structure_name, dataset_records, buckets_count=buckets_count)
                for op_name in operations:
                    elapsed = result[op_name]
                    times_by_op[op_name].append(elapsed)
                    rows.append([human_name, mode_name, op_name, attempt, f"{elapsed:.10f}"])

            for op_name in operations:
                avg_time = sum(times_by_op[op_name]) / len(times_by_op[op_name])
                rows.append([human_name, mode_name, op_name, "среднее", f"{avg_time:.10f}"])

    return rows


def save_results_csv(rows, filename: str = "results.csv"):
    with open(filename, "w", newline="", encoding="utf-8") as f:
        writer = csv.writer(f)
        writer.writerows(rows)


def main():
    rows = run_experiments(n=10000, buckets_count=2048, repeats=5)
    save_results_csv(rows, "results.csv")
    print("Saved results.csv")


if __name__ == "__main__":
    main()
