import numpy as np
import time
import os
import re
import json

# --- Helper Functions for Data Loading ---

def read_distance_matrix_from_file(file_path: str) -> np.ndarray:
    """
    Reads a pre-computed distance matrix from a .txt file.
    This function skips non-numeric lines, such as headers or the optimal distance line.
    """
    matrix_rows = []
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            for line in f:
                line = line.strip()
                # Skip empty lines or lines that do not start with a digit
                if not line or not line[0].isdigit():
                    continue
                
                try:
                    row = [float(num) for num in line.split()]
                    if row:
                        matrix_rows.append(row)
                except ValueError:
                    # Skip lines that cannot be fully converted to numbers
                    continue
    except FileNotFoundError:
        raise

    if not matrix_rows:
        raise ValueError(f"Could not parse any matrix data from {os.path.basename(file_path)}.")

    return np.array(matrix_rows)


def parse_optimal_distance_from_file(file_path: str) -> float | None:
    """
    Parses a pre-saved optimal distance from a file using regex.
    It looks for a line formatted like 'Total Distance: 1234.56'.
    """
    pattern = re.compile(r"Total Distance:\s*([\d.]+)")
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            for line in reversed(f.readlines()):
                match = pattern.search(line)
                if match:
                    return float(match.group(1))
    except (FileNotFoundError, IOError):
        return None
    return None

# EVOLVE-BLOCK-START

def solve_tsp_approximate(dist_matrix):
    """
    Finds an approximate solution to the TSP for a given distance matrix
    using the Random Insertion heuristic.
    """
    n = dist_matrix.shape[0]

    if n < 2:
        return np.array(range(n)), 0.0

    # 1. Initialization
    if n > 1:
        unvisited_cities = list(range(1, n))
        np.random.shuffle(unvisited_cities) # Randomize the insertion order
        # Start tour with city 0 and the first random city
        tour = [0, unvisited_cities.pop(0)]
    else: # Handle n=1 case
        return np.array([0]), 0.0


    # 2. Iteratively insert the remaining cities
    for city_to_insert in unvisited_cities:
        best_insertion_cost = float('inf')
        best_insertion_pos = -1

        for i in range(len(tour)):
            city1_idx = tour[i]
            city2_idx = tour[(i + 1) % len(tour)]

            cost = (
                dist_matrix[city1_idx, city_to_insert]
                + dist_matrix[city_to_insert, city2_idx]
                - dist_matrix[city1_idx, city2_idx]
            )

            if cost < best_insertion_cost:
                best_insertion_cost = cost
                best_insertion_pos = i + 1

        tour.insert(best_insertion_pos, city_to_insert)

    # 3. Calculate the final tour distance
    total_distance = sum(dist_matrix[tour[i], tour[(i + 1) % n]] for i in range(n))

    return np.array(tour), total_distance

# EVOLVE-BLOCK-END

if __name__ == "__main__":
    # ❗ **SET YOUR FOLDER PATH HERE**
    folder_path = 'all'
    
    if not os.path.isdir(folder_path):
        print(f"❌ Error: Folder not found at '{folder_path}'")
        exit()

    errors = []
    results_data = [] # List to store results for JSON output
    total_start_time = time.time()

    # Modified to look for .txt files
    print(f"🚀 Scanning for .txt files in '{os.path.abspath(folder_path)}'...")
    
    problem_files = sorted([f for f in os.listdir(folder_path) if f.endswith(".txt")])
    
    if not problem_files:
        # Modified to mention .txt files
        print("⚠️ No .txt files found in the specified directory.")
        exit()

    # --- Main Processing Loop ---
    for filename in problem_files:
        file_path = os.path.join(folder_path, filename)
        print("\n" + "-"*50)
        print(f"Processing: {filename}")
        
        try:
            # Step 1: Get the known optimal result from the file
            optimal_result = parse_optimal_distance_from_file(file_path)
            if optimal_result is None:
                print("   -> ⚠️ Warning: No saved optimal result found in file. Skipping.")
                continue

            # Step 2: Read the distance matrix directly from the file
            dist_matrix = read_distance_matrix_from_file(file_path)
            
            # Step 3: Run your heuristic to get a new result
            _, heuristic_result = solve_tsp_approximate(dist_matrix)
            
            # Step 4: Calculate the error and store it
            error = (heuristic_result - optimal_result) / optimal_result
            errors.append(error)
            
            # Store results in a dictionary for JSON output
            result_entry = {
                "filename": filename,
                "optimal_distance": optimal_result,
                "heuristic_distance": heuristic_result,
                "error": error
            }
            results_data.append(result_entry)
            
            print(f"   -> Optimal Result (from file): {optimal_result:.4f}")
            print(f"   -> Heuristic Result (this code): {heuristic_result:.4f}")
            print(f"   -> Error: {error:.2%}")

        except Exception as e:
            print(f"   -> ❌ An error occurred: {e}")

    # --- Save results to JSON file ---
    json_output_path = os.path.join(folder_path, 'random_insertion_result.json')
    print("\n" + "-"*50)
    print(f"💾 Saving detailed results to {json_output_path}...")
    try:
        with open(json_output_path, 'w', encoding='utf-8') as f:
            json.dump(results_data, f, indent=4)
        print("   -> ✅ Successfully saved.")
    except Exception as e:
        print(f"   -> ❌ Failed to save JSON file: {e}")

    # --- Final Summary ---
    total_execution_time = time.time() - total_start_time
    
    print("\n" + "="*50)
    print("🎉 All files processed. Final Report:")
    print("="*50)
    
    if errors:
        average_error = np.mean(errors)
        print(f"📂 Files Processed: {len(errors)}")
        print(f"📊 **Average Error:** **{average_error:.2%}**")
    else:
        print("No files were successfully processed to calculate an average error.")
        
    print(f"⏱️ **Total Execution Time:** **{total_execution_time:.4f} seconds**")