import geopandas as gpd
import pandas as pd
from shapely.geometry import LineString, Point
import os
import glob

def get_input(prompt, default=None, cast_type=str):
    """
    Prompts the user for input with an optional default value and casts it to the specified type.
    """
    while True:
        try:
            if default is not None:
                user_input = input(f"{prompt} [{default}]: ").strip()
                if user_input == "":
                    user_input = default
            else:
                user_input = input(f"{prompt}: ").strip()
            return cast_type(user_input)
        except ValueError:
            print(f"Input non valido. Inserisci un valore di tipo {cast_type.__name__}.")

def merge_files(input_folder, file_extension='csv', key_columns=None):
    """
    Merges multiple CSV or TXT files from a specified folder into a single DataFrame.
    """
    file_pattern = os.path.join(input_folder, f'*.{file_extension}')
    file_list = glob.glob(file_pattern)
    
    if not file_list:
        raise FileNotFoundError(f"Nessun file con estensione '{file_extension}' trovato in '{input_folder}'.")
    
    df_list = []
    for file in file_list:
        try:
            if file_extension == 'csv':
                df = pd.read_csv(file)
            elif file_extension == 'txt':
                df = pd.read_csv(file, delimiter='\t')  # Adjust delimiter if necessary
            else:
                raise ValueError("Estensione del file non supportata. Usa 'csv' o 'txt'.")
            
            # Optionally, add a source column to trace back the origin
            df['Source_File'] = os.path.basename(file)
            df_list.append(df)
        
        except Exception as e:
            print(f"Errore nella lettura di {file}: {e}")
    
    merged_df = pd.concat(df_list, ignore_index=True)
    merged_df.drop_duplicates(inplace=True)
    
    if key_columns:
        missing_columns = [col for col in key_columns if col not in merged_df.columns]
        if missing_columns:
            raise ValueError(f"Le seguenti colonne richieste non sono presenti nei file: {', '.join(missing_columns)}")
        merged_df = merged_df[key_columns]
    
    return merged_df

def dataframe_to_geodataframe(df, lon_column='X', lat_column='Y', crs='EPSG:4326'):
    """
    Converts a pandas DataFrame into a GeoDataFrame with Point geometries.
    """
    try:
        geometry = [Point(xy) for xy in zip(df[lon_column], df[lat_column])]
    except KeyError as e:
        raise KeyError(f"Colonna mancante per le coordinate: {e}")
    
    gdf = gpd.GeoDataFrame(df, geometry=geometry)
    gdf.set_crs(crs, inplace=True)
    return gdf

def export_to_shapefile(gdf, output_path):
    """
    Exports a GeoDataFrame to a shapefile.
    """
    try:
        # Ensure the directory exists
        os.makedirs(os.path.dirname(output_path), exist_ok=True)
        
        # Export to Shapefile
        gdf.to_file(output_path, driver='ESRI Shapefile')
        print(f"Fatto: Shapefile salvato in '{output_path}'.")
    except Exception as e:
        print(f"Errore nell'esportazione dello Shapefile: {e}")

def standardize_column_names(df):
    """
    Removes whitespace and standardizes column names to Title Case,
    keeping 'geometry' column unchanged.
    """
    try:
        df = df.rename(columns=lambda x: x.strip())
        df.columns = [col.strip() for col in df.columns]
        standardized_columns = []
        for col in df.columns:
            if col.lower() == 'geometry':
                standardized_columns.append('geometry')
            else:
                standardized_columns.append(col.title())
        df.columns = standardized_columns
        return df
    except Exception as e:
        raise Exception(f"Errore nella standardizzazione dei nomi delle colonne: {e}")

def categorize_gap(gap, median_gap, tolerance=0.2):
    """
    Categorize the gap based on its relation to the median_gap.
    """
    if gap > median_gap * (1 + tolerance):
        return "Significantly Large Gap"
    elif gap < median_gap * (1 - tolerance):
        return "Significantly Small Gap"
    else:
        return "Normal"

def main():
    while True:
        print("=== Conversione Punti in Percorsi ===\n")
        
        # Section 1: Creating Shapefile from Multiple Files
        try:
            create_shapefile = get_input(
                "Vuoi creare uno shapefile da più file CSV/TXT? (s/n)", 
                default='s', 
                cast_type=str
            ).lower()
            
            if create_shapefile == 's':
                # Step 1: Input Folder Path
                input_folder = input("Inserisci il percorso della cartella contenente i file CSV/TXT da unire: ").strip('"').strip("'")
                if not os.path.isdir(input_folder):
                    print("Errore: La cartella specificata non esiste.\n")
                    continue
                
                # Step 2: Select File Extension
                file_extension = get_input(
                    "Inserisci l'estensione dei file da unire ('csv' o 'txt')", 
                    default='csv', 
                    cast_type=str
                ).lower()
                if file_extension not in ['csv', 'txt']:
                    print("Errore: Estensione dei file non valida. Usa 'csv' o 'txt'.\n")
                    continue
                
                # Step 3: Define Expected Columns
                expected_columns_input = get_input(
                    "Inserisci i nomi delle colonne richieste separati da virgola (es. Time,Image,X,Y,Z,Roll,Pitch,Heading)", 
                    default='Time,Image,X,Y,Z,Roll,Pitch,Heading', 
                    cast_type=str
                )
                expected_columns = [col.strip() for col in expected_columns_input.split(',')]
                
                # Step 4: Merge Files
                try:
                    merged_df = merge_files(
                        input_folder, 
                        file_extension=file_extension, 
                        key_columns=expected_columns
                    )
                    print(f"I file sono stati uniti correttamente. Numero totale di record: {len(merged_df)}.\n")
                except Exception as e:
                    print(f"Errore nell'unire i file: {e}\n")
                    continue
                
                # Step 5: Convert to GeoDataFrame
                try:
                    # 'X' and 'Y' are assumed to be Longitude and Latitude
                    gdf = dataframe_to_geodataframe(
                        merged_df, 
                        lon_column='X', 
                        lat_column='Y'
                    )
                    print("DataFrame convertito correttamente in GeoDataFrame.\n")
                except Exception as e:
                    print(f"Errore nella conversione in GeoDataFrame: {e}\n")
                    continue
                
                # Step 6: Export to Shapefile
                output_base_default = os.path.splitext(os.path.basename(input_folder))[0] + "_new"
                output_base = get_input(
                    "Inserisci il nome per il nuovo shapefile (senza estensione)",
                    default=output_base_default,
                    cast_type=str
                )
                output_shp = os.path.abspath(output_base + ".shp")
                export_to_shapefile(gdf, output_shp)
            
            else:
                # If not creating shapefile, prompt for existing shapefile
                input_shapefile = get_input(
                    "Inserisci il percorso dello shapefile di input (.shp)", 
                    default=None, 
                    cast_type=str
                ).strip('"').strip("'")
                if not os.path.isfile(input_shapefile):
                    print("Errore: Lo shapefile specificato non esiste.\n")
                    continue
                try:
                    gdf = gpd.read_file(input_shapefile)
                    gdf = standardize_column_names(gdf)
                    print("Shapefile caricato e colonne standardizzate correttamente.\n")
                except Exception as e:
                    print(f"Errore nel caricamento dello shapefile: {e}\n")
                    continue
        except Exception as e:
            print(f"Errore durante la creazione dello shapefile: {e}\n")
            continue
        
        # Ensure 'Time' column exists and is numeric
        try:
            if 'Time' not in gdf.columns:
                print("Errore: La colonna 'Time' non è presente nello shapefile.\n")
                continue
            else:
                if gdf['Time'].dtype.kind not in 'biufc':
                    print(f"Errore: La colonna 'Time' non è numerica (tipo attuale: {gdf['Time'].dtype}).\n")
                    continue
                else:
                    print("Colonna 'Time' verificata come numerica.\n")
        except Exception as e:
            print(f"Errore nella verifica della colonna 'Time': {e}\n")
            continue
        
        # Step 7: Set Gap Threshold with Enhanced Prompt
        try:
            gap_threshold = get_input(
                "Inserisci la soglia del divario, ovvero il tempo massimo consentito (in secondi) tra due punti per considerarli nello stesso percorso",
                default=3.0,
                cast_type=float
            )
        except Exception as e:
            print(f"Errore nell'impostazione della soglia del divario: {e}\n")
            continue
        
        # Step 8: Set Group Field Name
        try:
            default_group_field = "PathID"
            group_field = get_input(
                f"Inserisci il nome per il campo del nuovo gruppo [default: {default_group_field}]: ",
                default=default_group_field,
                cast_type=str
            )
        except Exception as e:
            print(f"Errore nell'impostazione del nome del campo del gruppo: {e}\n")
            continue
        
        # Step 9: Add Group Field if not exists
        try:
            if group_field not in gdf.columns:
                gdf[group_field] = 0
        except Exception as e:
            print(f"Errore nell'aggiungere il campo del gruppo: {e}\n")
            continue
        
        # Step 10: Sort by 'Time'
        try:
            gdf_sorted = gdf.sort_values(by='Time').reset_index(drop=True)
        except Exception as e:
            print(f"Errore nell'ordinamento per il campo 'Time': {e}\n")
            continue
        
        # Step 11: Assign PathID and calculate temporal gaps
        try:
            current_group = 1
            previous_time = None
            path_ids = []
            gaps = []
            
            for idx, row in gdf_sorted.iterrows():
                current_time = row['Time']
                if pd.isna(current_time):
                    print(f"Avviso: Valore NaN rilevato nel campo Time all'indice {idx}. Assegnato al gruppo corrente.")
                    path_ids.append(current_group)
                    continue
                if previous_time is not None:
                    gap = current_time - previous_time
                    gaps.append({
                        "Entry_Index": idx - 1,
                        "Previous_Time": previous_time,
                        "Current_Time": current_time,
                        "Gap": gap
                    })
                    if gap > gap_threshold:
                        current_group += 1
                path_ids.append(current_group)
                previous_time = current_time
            
            gdf_sorted[group_field] = path_ids
        except Exception as e:
            print(f"Errore nell'assegnazione dei PathID e nel calcolo dei gap: {e}\n")
            continue
        
        # Step 12: Convert gaps to DataFrame for final log
        try:
            gaps_df = pd.DataFrame(gaps)
        except Exception as e:
            print(f"Errore nella creazione del DataFrame dei gap: {e}\n")
            continue
        
        # Step 13: Set output base name (without extension)
        try:
            default_output_base = os.path.splitext(os.path.basename(input_shapefile))[0] + "_new"
            output_base = get_input(
                "Inserisci il nome per il nuovo shapefile (senza estensione)",
                default=default_output_base,
                cast_type=str
            )
            output_shp = os.path.abspath(output_base + ".shp")
            output_kml = os.path.abspath(output_base + ".kml")
        except Exception as e:
            print(f"Errore nell'impostazione del nome dei file di output: {e}\n")
            continue
        
        # Step 14: Generate new connected paths and export
        try:
            # Filter groups with at least two points
            valid_groups = gdf_sorted.groupby(group_field).filter(lambda x: len(x) > 1)
    
            # Check if there are valid groups
            if valid_groups.empty:
                print("Errore: Nessun gruppo contiene più di un punto. Impossibile creare LineStrings.\n")
                continue
    
            # Group by PathID and create LineStrings
            paths = valid_groups.groupby(group_field)['geometry'].apply(lambda x: LineString(x.tolist())).reset_index()
    
            # Create GeoDataFrame for paths
            paths_gdf = gpd.GeoDataFrame(paths, geometry='geometry')
    
            # Set CRS to match input
            paths_gdf.set_crs(gdf_sorted.crs, inplace=True)
    
            # Calculate lengths in kilometers
            if not gdf_sorted.crs.is_projected:
                print("\nAvviso: Il CRS di input non è proiettato. Riproiezione a EPSG:3857 per i calcoli delle distanze.")
                paths_gdf = paths_gdf.to_crs(epsg=3857)
    
            paths_gdf['Length_km'] = paths_gdf.length / 1000  # Convert meters to kilometers
    
            # Calculate total distance
            total_distance_km = paths_gdf['Length_km'].sum()
            print(f"\nDistanza totale di tutti i percorsi: {total_distance_km:.3f} chilometri.")
    
            # Save paths as Shapefile
            export_to_shapefile(paths_gdf, output_shp)
    
            # Save paths as KML
            try:
                paths_gdf.to_file(output_kml, driver='KML')
                print(f"Fatto: I percorsi sono stati salvati in '{output_kml}'.\n")
            except Exception as e:
                print(f"Errore nel salvare i percorsi come KML: {e}\n")
                continue
    
            # Generate summary report
            try:
                # Calculate Start_Time and End_Time
                paths_gdf['Start_Time'] = gdf_sorted.groupby(group_field)['Time'].transform('min')
                paths_gdf['End_Time'] = gdf_sorted.groupby(group_field)['Time'].transform('max')
                summary_summary = paths_gdf[['PathID', 'Length_km', 'Start_Time', 'End_Time']].drop_duplicates()
    
                # Save summary as CSV
                summary_output = os.path.splitext(output_shp)[0] + "_summary.csv"
                summary_summary.to_csv(summary_output, index=False)
                print(f"Fatto: Il report di sintesi dei percorsi è stato salvato in '{summary_output}'.\n")
            except Exception as e:
                print(f"Errore nel generare il report di sintesi dei percorsi: {e}\n")
                continue
    
        # Step 15: Prepare final report
            try:
                report_lines = []
                report_lines.append("=== Report Finale ===\n")
                report_lines.append(f"Shapefile di input: {os.path.abspath(input_shapefile)}")
                report_lines.append(f"Shapefile di output: {output_shp}")
                report_lines.append(f"KML di output: {output_kml}")
                report_lines.append(f"Campo PathID: {group_field}")
                report_lines.append(f"Campo Time: {time_field}")
                report_lines.append(f"Soglia del divario: {gap_threshold}")
                report_lines.append(f"Distanza totale di tutti i percorsi: {total_distance_km:.3f} chilometri.\n")
    
                # Gap analysis
                if not gaps_df.empty:
                    median_gap = gaps_df['Gap'].median()
                    mean_gap = gaps_df['Gap'].mean()
                    std_gap = gaps_df['Gap'].std()
                    
                    # Place stats at the top of the report
                    report_lines.append("=== Statistiche Gap ===\n")
                    report_lines.append(f"Gap Mediano: {median_gap:.3f} unità")
                    report_lines.append(f"Gap Medio: {mean_gap:.3f} unità")
                    report_lines.append(f"Deviazione Standard dei Gap: {std_gap:.3f} unità\n")
                    
                    gaps_df['Observation'] = gaps_df['Gap'].apply(lambda x: categorize_gap(x, median_gap))
                    
                    # Summary of gap categories
                    category_counts = gaps_df['Observation'].value_counts()
                    report_lines.append("=== Sintesi delle Categorie dei Gap ===\n")
                    for category, count in category_counts.items():
                        report_lines.append(f"{category}: {count}")
                    report_lines.append("\n")
    
                    # Add gap table, limit to 100 entries
                    report_lines.append("=== Analisi dei Gap ===\n")
                    # Format gaps in a compact table
                    formatted_gaps = gaps_df.copy()
                    formatted_gaps['Entry'] = formatted_gaps.index + 1
                    formatted_gaps = formatted_gaps[['Entry', 'Previous_Time', 'Current_Time', 'Gap', 'Observation']]
                    formatted_gaps.rename(columns={
                        'Previous_Time': 'Time',
                        'Current_Time': 'Next Time',
                        'Gap': 'Gap (Seconds)'
                    }, inplace=True)
                    # Convert 'Entry' to string to allow '100+'
                    formatted_gaps['Entry'] = formatted_gaps['Entry'].astype(str)
                    # Limit to 100 entries
                    if len(formatted_gaps) > 100:
                        displayed_gaps = formatted_gaps.head(100).copy()
                        displayed_gaps.at[99, 'Entry'] = '100+'
                    else:
                        displayed_gaps = formatted_gaps
                    report_lines.append(displayed_gaps.to_string(index=False, float_format='{:,.3f}'.format))
                    report_lines.append("\n")
                else:
                    report_lines.append("Nessun gap calcolato.\n")
    
                # Add path summary without duplicate distance
                report_lines.append("=== Sintesi dei Percorsi ===\n")
                report_lines.append(summary_summary.to_string(index=False, float_format='{:,.3f}'.format))
                report_lines.append("\n")
    
                # Add the total km and number of PathIDs only once
                total_paths = summary_summary['PathID'].nunique()
                report_lines.append(f"Numero totale di PathID: {total_paths}\n")
    
                # Save final report as TXT
                final_report_path = os.path.splitext(output_shp)[0] + "_final_report.txt"
                with open(final_report_path, 'w', encoding='utf-8') as report_file:
                    for line in report_lines:
                        report_file.write(line + "\n")
    
                print(f"Fatto: Il report finale è stato salvato in '{final_report_path}'.\n")
            except Exception as e:
                print(f"Errore nella preparazione del report finale: {e}\n")
                continue
        
        # Step 16: Console summary at the end
            try:
                unique_path_ids = gdf_sorted[group_field].nunique()
                print(f"Numero totale di PathID creati: {unique_path_ids}")
                if not gaps_df.empty:
                    median_gap = gaps_df['Gap'].median()
                    print(f"Gap mediano: {median_gap:.3f} unità")
                else:
                    print("Nessun gap calcolato.")
                print(f"Distanza totale di tutti i percorsi: {total_distance_km:.3f} chilometri.\n")
            except Exception as e:
                print(f"Errore nella stampatura del riepilogo console: {e}\n")
                continue
        
            # Step 17: Continue or exit
            try:
                cont = get_input("Vuoi continuare? (s/n)", default='s', cast_type=str).strip().lower()
                if cont != 's':
                    print("Script terminato.\n")
                    break
            except Exception as e:
                print(f"Errore nella decisione di continuare: {e}\n")
                break

    if __name__ == "__main__":
        main()

it gives me this error 
PS C:\Users\SPL\Music> python.exe '.\nnew_test_promptfix_with csv_import.py'
  File "C:\Users\SPL\Music\nnew_test_promptfix_with csv_import.py", line 473
    if __name__ == "__main__":
SyntaxError: expected 'except' or 'finally' block