#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Thu Jul 30th 2025

@author: Marcel Weschke
@email: marcel.weschke@directbox.de
"""
# %% Load libraries
import os
import base64
import io
import datetime
from math import radians, sin, cos, sqrt, asin

import dash
from dash import dcc, html, Input, Output, Dash, State
import dash_bootstrap_components as dbc

import pandas as pd
import numpy as np
import plotly.express as px
import plotly.graph_objects as go
from scipy.interpolate import interp1d
import gpxpy
from fitparse import FitFile

# === Helper Functions ===
def list_files():
    """
    Listet alle .fit Files aus fit_files/ und .gpx Files aus gpx_files/ auf
    und sortiert sie nach Datum (neueste zuerst)
    """

    # Definiere Ordner und Dateierweiterungen
    folders_config = [
        {'folder': './fit_files', 'extensions': ['.fit'], 'type': 'FIT'},
        {'folder': './gpx_files', 'extensions': ['.gpx'], 'type': 'GPX'}
    ]

    all_file_options = []

    for config in folders_config:
        folder = config['folder']
        extensions = config['extensions']
        file_type = config['type']

        # Prüfe ob Ordner existiert
        if not os.path.exists(folder):
            print(f"Ordner {folder} existiert nicht!")
            continue

        # Hole alle Files mit den entsprechenden Erweiterungen
        try:
            all_files = os.listdir(folder)
            files = [f for f in all_files
                    if any(f.lower().endswith(ext) for ext in extensions)]
        except Exception as e:
            print(f"Fehler beim Lesen des Ordners {folder}: {e}")
            continue

        # Erstelle Optionen für diesen Ordner
        for f in files:
            file_path = os.path.join(folder, f)

            # Extrahiere Datum für Sortierung
            file_date = extract_date_from_file(f, file_path)

            # Erstelle Label mit Dateityp-Info
            try:
                size_mb = os.path.getsize(file_path) / (1024 * 1024)
                mod_time = datetime.datetime.fromtimestamp(os.path.getmtime(file_path))
                #label = f"[{file_type}] {f}"
                label = f"{f}"
                # Optional: Erweiterte Info (auskommentiert für sauberere Ansicht)
                # label = f"[{file_type}] {f} ({size_mb:.1f}MB - {mod_time.strftime('%d.%m.%Y %H:%M')})"
            except:
                #label = f"[{file_type}] {f}"
                label = f"{f}"

            all_file_options.append({
                'label': label,
                'value': file_path,
                'date': file_date,
                'type': file_type
            })

    # Sortiere alle Files nach Datum (neueste zuerst)
    all_file_options.sort(key=lambda x: x['date'], reverse=True)

    # Entferne 'date' und 'type' aus den finalen Optionen (nur für Sortierung gebraucht)
    final_options = [{'label': opt['label'], 'value': opt['value']}
                    for opt in all_file_options]

    # Fallback wenn keine Files gefunden
    if not final_options:
        return [{'label': 'Keine .fit oder .gpx Dateien gefunden', 'value': 'NO_FILES'}]

    return final_options

def extract_date_from_file(filename, file_path):
    """Extrahiert Datum aus Filename für Sortierung"""
    try:
        # Versuche verschiedene Datumsformate im Dateinamen
        # Format: dd.mm.yyyy
        return datetime.datetime.strptime(filename[:10], '%d.%m.%Y')
    except ValueError:
        try:
            # Format: yyyy-mm-dd
            return datetime.datetime.strptime(filename[:10], '%Y-%m-%d')
        except ValueError:
            try:
                # Format: yyyymmdd
                return datetime.datetime.strptime(filename[:8], '%Y%m%d')
            except ValueError:
                try:
                    # Format: yyyy_mm_dd
                    return datetime.datetime.strptime(filename[:10], '%Y_%m_%d')
                except ValueError:
                    try:
                        # Format: dd-mm-yyyy
                        return datetime.datetime.strptime(filename[:10], '%d-%m-%Y')
                    except ValueError:
                        # Wenn kein Datum erkennbar, nutze Datei-Änderungsdatum
                        try:
                            return datetime.datetime.fromtimestamp(os.path.getmtime(file_path))
                        except:
                            return datetime.datetime.min


def haversine(lon1, lat1, lon2, lat2):
    """
    Berechnet die Entfernung zwischen zwei GPS-Koordinaten in km
    """
    R = 6371  # Erdradius in km
    dlon = radians(lon2 - lon1)
    dlat = radians(lat2 - lat1)
    a = sin(dlat/2)**2 + cos(radians(lat1)) * cos(radians(lat2)) * sin(dlon/2)**2
    return 2 * R * asin(sqrt(a))


########
# FIT
########
def process_fit(file_path):
    """
    Verarbeitet eine FIT-Datei und erstellt einen DataFrame
    """
    if file_path in ['NO_FILE', 'NO_FOLDER', 'ERROR']:
        print(f"Ungültiger Dateipfad: {file_path}")
        return pd.DataFrame()

    if not os.path.exists(file_path):
        print(f"Datei nicht gefunden: {file_path}")
        return pd.DataFrame()

    try:
        fit_file = FitFile(file_path)
        print(f"\nVerarbeite FIT-Datei: {file_path}")

        # Sammle alle record-Daten
        records = []
        for record in fit_file.get_messages("record"):
            record_data = {}
            for data in record:
                # Sammle alle verfügbaren Datenfelder
                record_data[data.name] = data.value
            records.append(record_data)

        if not records:
            print("Keine Aufzeichnungsdaten in der FIT-Datei gefunden")
            return pd.DataFrame()

        # Erstelle DataFrame
        df = pd.DataFrame(records)
        print(f"DataFrame erstellt mit {len(df)} Zeilen und Spalten: {list(df.columns)}")

        # Debugging: Schaue welche Spalten verfügbar sind
        # print(f"Verfügbare Spalten: {df.columns.tolist()}")     # Uncomment if needed - DEBUG purpose!

        # Suche nach Heart Rate in verschiedenen Formaten
        possible_hr_cols = [col for col in df.columns if 'heart' in col.lower() or 'hr' in col.lower()]
        # print(f"Mögliche Heart Rate Spalten: {possible_hr_cols}")     # Uncomment if needed - DEBUG purpose!

        # Standard-Spaltennamen für verschiedene FIT-Formate
        lat_cols = ['position_lat', 'lat', 'latitude']
        lon_cols = ['position_long', 'lon', 'longitude']
        elev_cols = ['altitude', 'elev', 'elevation', 'enhanced_altitude']
        time_cols = ['timestamp', 'time']
        hr_cols = ['heart_rate', 'hr'] + possible_hr_cols
        speed_cols = ['speed', 'enhanced_speed']
        dist_cols = ['distance', 'total_distance']

        # Finde die richtigen Spaltennamen
        lat_col = next((col for col in lat_cols if col in df.columns), None)
        lon_col = next((col for col in lon_cols if col in df.columns), None)
        elev_col = next((col for col in elev_cols if col in df.columns), None)
        time_col = next((col for col in time_cols if col in df.columns), None)
        hr_col = next((col for col in hr_cols if col in df.columns), None)
        speed_col = next((col for col in speed_cols if col in df.columns), None)

        # Prüfe ob wichtige Daten vorhanden sind
        if not lat_col or not lon_col or not time_col:
            raise ValueError(f"Wichtige Daten fehlen! Lat: {lat_col}, Lon: {lon_col}, Time: {time_col}")

        # Benenne Spalten einheitlich um
        df = df.rename(columns={
            lat_col: 'lat',
            lon_col: 'lon',
            elev_col: 'elev' if elev_col else None,
            time_col: 'time',
            hr_col: 'heart_rate' if hr_col else None,
            speed_col: 'speed_ms' if speed_col else None
        })

        # FIT lat/lon sind oft in semicircles - konvertiere zu Grad
        if df['lat'].max() > 180:  # Semicircles detection
            df['lat'] = df['lat'] * (180 / 2**31)
            df['lon'] = df['lon'] * (180 / 2**31)

        # Entferne Zeilen ohne GPS-Daten
        df = df.dropna(subset=['lat', 'lon', 'time']).reset_index(drop=True)

        # Basic cleanup
        df['time'] = pd.to_datetime(df['time'])
        df['time_loc'] = df['time'].dt.tz_localize(None)
        df['time_diff'] = df['time'] - df['time'].iloc[0]
        df['time_diff_sec'] = df['time_diff'].dt.total_seconds()
        df['duration_hms'] = df['time_diff'].apply(lambda td: str(td).split('.')[0])

        # Cumulative distance (km)
        distances = [0]
        for i in range(1, len(df)):
            d = haversine(df.loc[i-1, 'lon'], df.loc[i-1, 'lat'], df.loc[i, 'lon'], df.loc[i, 'lat'])
            distances.append(distances[-1] + d)
        df['cum_dist_km'] = distances

        # Elevation handling
        if 'elev' in df.columns:
            df['elev'] = df['elev'].bfill()
            df['delta_elev'] = df['elev'].diff().fillna(0)
            df['rel_elev'] = df['elev'] - df['elev'].iloc[0]
        else:
            # Fallback wenn keine Elevation vorhanden
            df['elev'] = 0
            df['delta_elev'] = 0
            df['rel_elev'] = 0

        # Speed calculation
        if 'speed_ms' in df.columns:
            # Konvertiere m/s zu km/h
            df['speed_kmh'] = df['speed_ms'] * 3.6
        else:
            # Fallback: Berechne Speed aus GPS-Daten
            df['delta_t'] = df['time'].diff().dt.total_seconds()
            df['delta_d'] = df['cum_dist_km'].diff()
            df['speed_kmh'] = (df['delta_d'] / df['delta_t']) * 3600
            df['speed_kmh'] = df['speed_kmh'].replace([np.inf, -np.inf], np.nan)

        # Velocity (used in pace calculations)
        df['vel_kmps'] = np.gradient(df['cum_dist_km'], df['time_diff_sec'])

        # Smoothed speed (Gaussian rolling)
        df['speed_kmh_smooth'] = df['speed_kmh'].rolling(window=10, win_type="gaussian", center=True).mean(std=2)


        # Heart rate handling (NEU!)
        # ##############
        # UPDATE: Da NaN-Problem mit heart_rate, manuell nochmal neu einlesen und überschreiben:
        # save heart rate data into variable
        heart_rate = []
        for record in fit_file.get_messages("record"):
            # Records can contain multiple pieces of data (ex: timestamp, latitude, longitude, etc)
            for data in record:
                # Print the name and value of the data (and the units if it has any)
                if data.name == 'heart_rate':
                    heart_rate.append(data.value)
        # Hier variable neu überschrieben:
        df = safe_add_column_to_dataframe(df, 'heart_rate', heart_rate)
        # ##############

        # ######################################
        # IF issues with heart_rate values, usw these DEBUG prints:
        #print(heart_rate)
        if 'heart_rate' in df.columns:
            df['heart_rate'] = pd.to_numeric(df['heart_rate'], errors='coerce')
            df['hr_smooth'] = df['heart_rate'].rolling(window=5, center=True).mean()
        #    print(f"Heart rate range: {df['heart_rate'].min():.0f} - {df['heart_rate'].max():.0f} bpm")     # Uncomment if needed - DEBUG purpose!
        else:
            print("Keine Heart Rate Daten gefunden!")
            df['heart_rate'] = np.nan
            df['hr_smooth'] = np.nan

            print(f"Verarbeitete FIT-Datei: {len(df)} Datenpunkte")
            print(f"Distanz: {df['cum_dist_km'].iloc[-1]:.2f} km")
            print(f"Dauer: {df['duration_hms'].iloc[-1]}")
        # ######################################

        return df

    except Exception as e:
        print(f"Fehler beim Verarbeiten der FIT-Datei {file_path}: {str(e)}")
        return pd.DataFrame()



########
# GPX
########
def process_gpx(file_path):
    """
    Verarbeitet GPX-Dateien und gibt DataFrame im gleichen Format wie process_fit() zurück
    """
    import gpxpy
    import gpxpy.gpx

    try:
        with open(file_path, 'r', encoding='utf-8') as gpx_file:
            gpx = gpxpy.parse(gpx_file)

        print(f"Verarbeite GPX-Datei: {file_path}")

        # Sammle GPS-Punkte aus allen Tracks/Segments
        points_data = []
        for track in gpx.tracks:
            for segment in track.segments:
                for point in segment.points:
                    points_data.append({
                        'time': point.time,
                        'lat': point.latitude,
                        'lon': point.longitude,
                        'elev': point.elevation if point.elevation else 0,
                        'heart_rate': None  # GPX hat normalerweise keine HR-Daten
                    })

        if not points_data:
            print("Keine GPS-Daten in GPX-Datei gefunden")
            return pd.DataFrame()

        # Erstelle DataFrame
        df = pd.DataFrame(points_data)
        print(f"GPX DataFrame erstellt mit {len(df)} Zeilen")

        # Sortiere nach Zeit
        df = df.sort_values('time').reset_index(drop=True)

        # Zeit-Verarbeitung (wie in deiner FIT-Funktion)
        df['time'] = pd.to_datetime(df['time'])
        df['time_loc'] = df['time'].dt.tz_localize(None)
        df['time_diff'] = df['time'] - df['time'].iloc[0]
        df['time_diff_sec'] = df['time_diff'].dt.total_seconds()
        df['duration_hms'] = df['time_diff'].apply(lambda td: str(td).split('.')[0])

        # Kumulative Distanz (gleiche Logik wie in deiner FIT-Funktion)
        distances = [0]
        for i in range(1, len(df)):
            d = haversine(df.loc[i-1, 'lon'], df.loc[i-1, 'lat'], df.loc[i, 'lon'], df.loc[i, 'lat'])
            distances.append(distances[-1] + d)
        df['cum_dist_km'] = distances

        # Elevation (gleiche Logik wie in deiner FIT-Funktion)
        df['elev'] = df['elev'].bfill()
        df['delta_elev'] = df['elev'].diff().fillna(0)
        df['rel_elev'] = df['elev'] - df['elev'].iloc[0]

        # Speed-Berechnung (gleiche Logik wie dein Fallback)
        df['delta_t'] = df['time'].diff().dt.total_seconds()
        df['delta_d'] = df['cum_dist_km'].diff()
        df['speed_kmh'] = (df['delta_d'] / df['delta_t']) * 3600
        df['speed_kmh'] = df['speed_kmh'].replace([np.inf, -np.inf], np.nan)

        # Velocity (wie in deiner FIT-Funktion)
        df['vel_kmps'] = np.gradient(df['cum_dist_km'], df['time_diff_sec'])

        # Smoothed speed (wie in deiner FIT-Funktion)
        df['speed_kmh_smooth'] = df['speed_kmh'].rolling(window=10, win_type="gaussian", center=True).mean(std=2)

        # Heart rate (GPX hat keine, also NaN wie dein Fallback)
        df['heart_rate'] = np.nan
        df['hr_smooth'] = np.nan

        print(f"Verarbeitete GPX-Datei: {len(df)} Datenpunkte")
        print(f"Distanz: {df['cum_dist_km'].iloc[-1]:.2f} km")
        print(f"Dauer: {df['duration_hms'].iloc[-1]}")

        return df

    except Exception as e:
        print(f"Fehler beim Verarbeiten der GPX-Datei {file_path}: {str(e)}")
        return pd.DataFrame()

# NEUE UNIVERSELLE WRAPPER-FUNKTION (nutzt deine bestehenden Funktionen!)
def process_selected_file(file_path):
    """
    Universelle Funktion die automatisch FIT oder GPX verarbeitet
    """
    if not file_path or file_path in ['NO_FILES', 'NO_FOLDER', 'ERROR']:
        return pd.DataFrame()

    # Bestimme Dateityp
    if file_path.lower().endswith('.fit'):
        # NUTZT DEINE ORIGINALE FUNKTION!
        return process_fit(file_path)
    elif file_path.lower().endswith('.gpx'):
        # Nutzt die neue GPX-Funktion
        return process_gpx(file_path)
    else:
        print(f"Unbekannter Dateityp: {file_path}")
        return pd.DataFrame()




def safe_add_column_to_dataframe(df, column_name, values):
    """
    Fügt eine Spalte sicher zu einem DataFrame hinzu, auch wenn die Längen nicht übereinstimmen
    """
    if df.empty:
        return df

    df_len = len(df)
    values_len = len(values) if hasattr(values, '__len__') else 0

    if values_len == df_len:
        # Perfekt - gleiche Länge
        df[column_name] = values
    elif values_len > df_len:
        # Zu viele Werte - kürze sie
        print(f"WARNUNG: {column_name} hat {values_len} Werte, DataFrame hat {df_len} Zeilen. Kürze Werte.")
        df[column_name] = values[:df_len]
    elif values_len < df_len:
        # Zu wenige Werte - fülle mit NaN auf
        print(f"WARNUNG: {column_name} hat {values_len} Werte, DataFrame hat {df_len} Zeilen. Fülle mit NaN auf.")
        extended_values = list(values) + [None] * (df_len - values_len)
        df[column_name] = extended_values
    else:
        # Keine Werte - fülle mit NaN
        print(f"WARNUNG: Keine Werte für {column_name}. Fülle mit NaN.")
        df[column_name] = [None] * df_len

    return df




# =============================================================================
# INFO BANNER
# =============================================================================
def create_info_banner(df):
    # Total distance in km
    total_distance_km = df['cum_dist_km'].iloc[-1]

    # Total time as timedelta
    total_seconds = df['time_diff_sec'].iloc[-1]
    hours, remainder = divmod(int(total_seconds), 3600)
    minutes, seconds = divmod(remainder, 60)
    formatted_total_time = f"{hours:02d}:{minutes:02d}:{seconds:02d}"

    # Average pace (min/km)
    if total_distance_km > 0:
        pace_sec_per_km = total_seconds / total_distance_km
        pace_min = int(pace_sec_per_km // 60)
        pace_sec = int(pace_sec_per_km % 60)
        formatted_pace = f"{pace_min}:{pace_sec:02d} min/km"
    else:
        formatted_pace = "N/A"

    # Build the info banner layout
    info_banner = html.Div([
        html.Div([
            html.H4("Total Distance", style={'margin-bottom': '5px'}),
            html.H2(f"{total_distance_km:.2f} km")
        ], style={'width': '30%', 'display': 'inline-block', 'textAlign': 'center'}),

        html.Div([
            html.H4("Total Time", style={'margin-bottom': '5px'}),
            html.H2(formatted_total_time)
        ], style={'width': '30%', 'display': 'inline-block', 'textAlign': 'center'}),

        html.Div([
            html.H4("Average Pace", style={'margin-bottom': '5px'}),
            html.H2(formatted_pace)
        ], style={'width': '30%', 'display': 'inline-block', 'textAlign': 'center'}),
    ], style={
        'display': 'flex',
        'justifyContent': 'space-around',
        'backgroundColor': '#1e1e1e',
        'color': 'white',
        'padding': '5px',
        'marginBottom': '5px',
        'borderRadius': '10px',
        'width': '100%',
        #'maxWidth': '1200px',
        'margin': 'auto'
    })

    return info_banner
  

# =============================================================================
# START OF THE PLOTS
# =============================================================================
def create_map_plot(df):
    fig = px.line_map(
    df,
    lat='lat',
    lon='lon',
    zoom=13.5,
    height=800
    )
    
    fig.update_traces(
        hovertemplate=(
            #"Time: %{customdata[0]}<br>" +
            "Distance (Km): %{customdata[0]:.2f}<br>" +
            "Speed (Km/h): %{customdata[1]:.2f}<br>" +
            "Heart Rate (bpm): %{customdata[2]}<br>" +
            "Elapsed Time: %{customdata[3]}<extra></extra>"
        ),
        #customdata=df[['time', 'cum_dist_km', 'duration_hms']]
        customdata=df[['cum_dist_km', 'speed_kmh', 'heart_rate', 'duration_hms']]
    )
    # Define map style and the line ontop
    fig.update_layout(map_style="open-street-map")  #My-Fav: open-street-map, satellite-streets, dark, white-bg
    # Possible Options:
    # 'basic', 'carto-darkmatter', 'carto-darkmatter-nolabels', 'carto-positron', 'carto-positron-nolabels', 'carto-voyager', 'carto-voyager-nolabels', 'dark', 'light', 'open-street-map', 'outdoors', 'satellite', 'satellite-streets', 'streets', 'white-bg'.
    fig.update_traces(line=dict(color="#f54269", width=3))

    # Start / Stop marker
    start = df.iloc[0]
    end = df.iloc[-1]
    fig.add_trace(go.Scattermap(
        lat=[start['lat']], lon=[start['lon']], mode='markers+text',
        marker=dict(size=12, color='#fca062'), text=['Start'], name='Start', textposition='bottom left'
    ))
    fig.add_trace(go.Scattermap(
        lat=[end['lat']], lon=[end['lon']], mode='markers+text',
        marker=dict(size=12, color='#b9fc62'), text=['Stop'], name='Stop', textposition='bottom left'
    ))
    # THIS IS MY ELEVATION-PLOT SHOW POSITION-MARKER IN MAP-PLOT:
    fig.add_trace(go.Scattermap(
        lat=[],
        lon=[],
        mode="markers",
        marker=dict(size=18, color="#42B1E5", symbol="circle"),
        name="Hovered Point"
    ))
    # KOMPAKTE LAYOUT-EINSTELLUNGEN
    fig.update_layout(
        paper_bgcolor='#1e1e1e',
        font=dict(color='white'),
        # Margins reduzieren für kompakteren Plot
        margin=dict(l=60, r=45, t=10, b=50),  # Links, Rechts, Oben, Unten
        # Plotly-Toolbar konfigurieren
        showlegend=True,
        # Kompakte Legend
        legend=dict(
            orientation='h',         # horizontal layout
            yanchor='top',
            y=-0.02,                 # move legend below the map
            xanchor='center',
            x=0.5,
            font=dict(color='white', size=10)  # Kleinere Schrift
        )
    )

    return fig




# #####################
def create_elevation_plot(df, smooth_points=500):
    # Originale Daten
    x = df['time']
    y = df['rel_elev']

    # Einfache Glättung: nur Y-Werte glätten, X-Werte beibehalten
    if len(y) >= 4:  # Genug Punkte für cubic interpolation
        y_numeric = y.to_numpy()
        # Nur gültige Y-Punkte für Interpolation
        mask = ~np.isnan(y_numeric)
        if np.sum(mask) >= 4:  # Genug gültige Punkte
            # Index-basierte Interpolation für Y-Werte
            valid_indices = np.where(mask)[0]
            valid_y = y_numeric[mask]
            # Interpolation über die Indizes
            f = interp1d(valid_indices, valid_y, kind='cubic', bounds_error=False, fill_value='extrapolate')
            # Neue Y-Werte für alle ursprünglichen X-Positionen
            all_indices = np.arange(len(y))
            y_smooth = f(all_indices)
            # Originale X-Werte beibehalten
            x_smooth = x
        else:
            # Fallback: originale Daten
            x_smooth, y_smooth = x, y
    else:
        # Zu wenige Punkte: originale Daten verwenden
        x_smooth, y_smooth = x, y

    fig = go.Figure()

    # Separate Behandlung für positive und negative Bereiche
    y_array = np.array(y_smooth)
    x_array = np.array(x_smooth)

    # Positive Bereiche (Anstiege) - Gradient von transparent unten zu grün oben
    positive_mask = y_array >= 0
    if np.any(positive_mask):
        # Nulllinie für positive Bereiche
        fig.add_trace(go.Scatter(
            x=x_array,
            y=np.zeros_like(y_array),
            mode='lines',
            line=dict(width=0),
            hoverinfo='skip',
            showlegend=False
        ))

        # Positive Bereiche mit Gradient nach oben
        fig.add_trace(go.Scatter(
            x=x_array,
            y=np.where(y_array >= 0, y_array, 0),  # Nur positive Werte
            fill='tonexty',  # Fill zur vorherigen Trace (Nulllinie)
            mode='lines',
            line=dict(width=0),
            fillgradient=dict(
                type="vertical",
                colorscale=[
                    (0.0, "rgba(17, 17, 17, 0.0)"),    # Transparent unten (bei y=0)
                    (1.0, "rgba(43, 82, 144, 0.8)")     # Blau oben (bei maximaler Höhe)  Grün: 73, 189, 115
                ]
            ),
            hoverinfo='skip',
            showlegend=False
        ))

    # Negative Bereiche (Abstiege) - Gradient von grün unten zu transparent oben
    negative_mask = y_array < 0
    if np.any(negative_mask):
        # Nulllinie für negative Bereiche
        fig.add_trace(go.Scatter(
            x=x_array,
            y=np.where(y_array < 0, y_array, 0),  # Nur negative Werte
            mode='lines',
            line=dict(width=0),
            hoverinfo='skip',
            showlegend=False
        ))

        # Negative Bereiche mit Gradient nach unten
        fig.add_trace(go.Scatter(
            x=x_array,
            y=np.zeros_like(y_array),
            fill='tonexty',  # Fill zur vorherigen Trace (negative Werte)
            mode='lines',
            line=dict(width=0),
            fillgradient=dict(
                type="vertical",
                colorscale=[
                    (0.0, "rgba(43, 82, 144, 0.8)"),    # Blau unten (bei minimaler Tiefe)
                    (1.0, "rgba(17, 17, 17, 0.0)")     # Transparent oben (bei y=0)
                ]
            ),
            hoverinfo='skip',
            showlegend=False
        ))

    # Hauptlinie (geglättet) - über allem
    fig.add_trace(go.Scatter(
        x=x_smooth,
        y=y_smooth,
        mode='lines',
        line=dict(color='#2b5290', width=2),  # Weiße Linie für bessere Sichtbarkeit
        name='Elevation',
        showlegend=False
    ))

    # Add horizontal reference line at y=0
    fig.add_shape(
        type='line',
        x0=df['time_loc'].iloc[0],
        x1=df['time_loc'].iloc[-1],
        y0=0,
        y1=0,
        line=dict(color='gray', width=1, dash='dash'),
        name='Durchschnittstempo'
    )

    # Layout im Dark Theme
    fig.update_layout(
        title=dict(text='Höhenprofil (relativ zum Ausgangswert: 0m)', font=dict(size=16, color='white')),
        xaxis_title='Zeit',
        yaxis_title='Höhe relativ zum Start (m)',
        template='plotly_dark',
        paper_bgcolor='#1e1e1e',
        plot_bgcolor='#111111',
        font=dict(color='white'),
        margin=dict(l=40, r=40, t=50, b=40),
        height=400,
        uirevision='constant',  # Avoiding not needed Re-renderings
    )

    return fig

# Alte Version - normaler gradient fill between:
# def create_elevation_plot(df, smooth_points=500):
#     # Originale Daten
#     x = df['time']
#     y = df['rel_elev']
#
#     # Einfache Glättung: nur Y-Werte glätten, X-Werte beibehalten
#     if len(y) >= 4:  # Genug Punkte für cubic interpolation
#         y_numeric = y.to_numpy()
#
#         # Nur gültige Y-Punkte für Interpolation
#         mask = ~np.isnan(y_numeric)
#
#         if np.sum(mask) >= 4:  # Genug gültige Punkte
#             # Index-basierte Interpolation für Y-Werte
#             valid_indices = np.where(mask)[0]
#             valid_y = y_numeric[mask]
#
#             # Interpolation über die Indizes
#             f = interp1d(valid_indices, valid_y, kind='cubic',
#                         bounds_error=False, fill_value='extrapolate')
#
#             # Neue Y-Werte für alle ursprünglichen X-Positionen
#             all_indices = np.arange(len(y))
#             y_smooth = f(all_indices)
#
#             # Originale X-Werte beibehalten
#             x_smooth = x
#         else:
#             # Fallback: originale Daten
#             x_smooth, y_smooth = x, y
#     else:
#         # Zu wenige Punkte: originale Daten verwenden
#         x_smooth, y_smooth = x, y
#
#     fig = go.Figure()
#
#     # Fläche unter der Kurve (mit geglätteten Daten)
#     fig.add_trace(go.Scatter(
#         x=x_smooth, y=y_smooth,
#         mode='lines',
#         line=dict(color='#1CAF50'),      # Fill between color!
#         fill='tozeroy',
#         #fillcolor='rgba(226, 241, 248)',
#         hoverinfo='skip',
#         showlegend=False
#     ))
#
#     # Hauptlinie (geglättet)
#     fig.add_trace(go.Scatter(
#         x=x_smooth, y=y_smooth,
#         mode='lines',
#         line=dict(color='#084C20', width=2), # Line color!
#         name='Elevation',
#         showlegend=False
#     ))
#
#     # SUPDER IDEE, ABER GEHT NICHT WEGE NEUEN smoothed POINTS! GEHT NUR BEI X
#     #fig.update_traces(
#     #    hovertemplate=(
#     #        #"Time: %{customdata[0]}<br>" +
#     #        "Distance (km): %{customdata[0]:.2f}<br>" +
#     #        "Elevation: %{customdata[1]}<extra></extra>" +
#     #        "Elapsed Time: %{customdata[2]}<extra></extra>"
#     #    ),
#     #    customdata=df[['cum_dist_km','elev', 'time']]
#     #
#
#     # Layout im Dark Theme
#     fig.update_layout(
#         title=dict(text='Höhenprofil relativ zum Startwert', font=dict(size=16, color='white')),
#         xaxis_title='Zeit',
#         yaxis_title='Höhe relativ zum Start (m)',
#         template='plotly_dark',
#         paper_bgcolor='#1e1e1e',
#         plot_bgcolor='#111111',
#         font=dict(color='white'),
#         margin=dict(l=40, r=40, t=50, b=40),
#         height=400
#     )
#
#     return fig
# #####################





def create_deviation_plot(df):  #Distanz-Zeit-Diagramm
    # Compute mean velocity in km/s
    vel_kmps_mean = df['cum_dist_km'].iloc[-1] / df['time_diff_sec'].iloc[-1]
    # Expected cumulative distance assuming constant mean velocity
    df['cum_dist_km_qmean'] = df['time_diff_sec'] * vel_kmps_mean
    # Deviation from mean velocity distance
    df['del_dist_km_qmean'] = df['cum_dist_km'] - df['cum_dist_km_qmean']
    # Plot the deviation
    fig = px.line(
        df,
        x='time_loc',
        y='del_dist_km_qmean',
        labels={
            'time_loc': 'Zeit',
            'del_dist_km_qmean': 'Δ Strecke (km)'
        },
        template='plotly_dark',
    )
    fig.update_layout(
        title=dict(text='Abweichung von integriertem Durchschnittstempo', font=dict(size=16)),
        yaxis_title='Abweichung (km)',
        xaxis_title='Zeit',
        height=400,
        paper_bgcolor='#1e1e1e',
        plot_bgcolor='#111111',
        font=dict(color='white', size=14),
        margin=dict(l=40, r=40, t=50, b=40),
        uirevision='constant',  # Avoiding not needed Re-renderings
    )
    # Add horizontal reference line at y=0
    fig.add_shape(
        type='line',
        x0=df['time_loc'].iloc[0],
        x1=df['time_loc'].iloc[-1],
        y0=0,
        y1=0,
        line=dict(color='gray', width=1, dash='dash'),
        name='Durchschnittstempo'
    )
    return fig


def create_speed_plot(df):
    mask = df['speed_kmh_smooth'].isna()
    mean_speed_kmh = df['speed_kmh'].mean()
    fig = go.Figure()
    fig.add_trace(go.Scatter(
        x=df['time'][~mask],
        y=df['speed_kmh_smooth'][~mask],
        mode='lines',
        name='Geglättete Geschwindigkeit',
        line=dict(color='royalblue')
    ))
    fig.update_layout(
        title=dict(text=f'Tempo über die Zeit (geglättet) - Ø {mean_speed_kmh:.2f} km/h', font=dict(size=16)),
        xaxis=dict(title='Zeit', tickformat='%H:%M', type='date'),
        yaxis=dict(title='Geschwindigkeit (km/h)', rangemode='tozero'),
        template='plotly_dark',
        paper_bgcolor='#1e1e1e',
        plot_bgcolor='#111111',
        font=dict(color='white'),
        margin=dict(l=40, r=40, t=40, b=40),
        uirevision='constant',  # Avoiding not needed Re-renderings
    )
    # Add horizontal reference line at y=mean_speed_kmh
    fig.add_shape(
        type='line',
        x0=df['time_loc'].iloc[0],
        x1=df['time_loc'].iloc[-1],
        y0=mean_speed_kmh,
        y1=mean_speed_kmh,
        line=dict(color='gray', width=1, dash='dash'),
        name='Durchschnittstempo'
    )
    return fig







# heart_rate Plot NEW !!!
def create_heart_rate_plot(df):
    # Maske für gültige Heart Rate Daten
    mask = df['hr_smooth'].isna()

    # Durchschnittliche Heart Rate berechnen (nur gültige Werte)
    valid_hr = df['heart_rate'].dropna()
    if len(valid_hr) > 0:
        mean_hr = valid_hr.mean()
        min_hr = valid_hr.min()
        max_hr = valid_hr.max()
    else:
        mean_hr = 0
        min_hr = 0
        max_hr = 0

    fig = go.Figure()

    # Heart Rate Linie (geglättet)
    fig.add_trace(go.Scatter(
        x=df['time'][~mask],
        y=df['hr_smooth'][~mask],
        mode='lines',
        #name='Geglättete Herzfrequenz',
        line=dict(color='#ff2c48', width=2),
        showlegend=False,
        hovertemplate=(
            "Zeit: %{x}<br>" +
            "Herzfrequenz: %{y:.0f} bpm<br>" +
            "<extra></extra>"
        )
    ))

    # # Optional: Raw Heart Rate als dünnere, transparente Linie
    # if not df['heart_rate'].isna().all():
    #     fig.add_trace(go.Scatter(
    #         x=df['time'],
    #         y=df['heart_rate'],
    #         mode='lines',
    #         name='Raw Herzfrequenz',
    #         line=dict(color='#E43D70', width=1, dash='dot'),
    #         opacity=0.3,
    #         showlegend=False,
    #         hoverinfo='skip'
    #     ))

    # Durchschnittslinie
    if mean_hr > 0:
        fig.add_shape(
            type='line',
            x0=df['time_loc'].iloc[0],
            x1=df['time_loc'].iloc[-1],
            y0=mean_hr,
            y1=mean_hr,
            line=dict(color='gray', width=1, dash='dash'),
        )

        # Annotation für Durchschnittswert
        fig.add_annotation(
            x=df['time_loc'].iloc[int(len(df) * 0.5)],  # Bei 50% der Zeit
            y=mean_hr,
            text=f"Ø {mean_hr:.0f} bpm",
            showarrow=True,
            arrowhead=2,
            arrowcolor="gray",
            bgcolor="rgba(128,128,128,0.1)",
            bordercolor="gray",
            font=dict(color="white", size=10)
        )

    # Heart Rate Zonen (optional)
    if mean_hr > 0:
        # Geschätzte maximale Herzfrequenz (Beispiel: 200 bpm)
        max_hr_estimated = 200  # oder z. B. 220 - alter

        ## Definiere feste HR-Zonen in BPM
        #zones = [
        #    {"name": "Zone 1", "lower": 0,   "upper": 124, "color": "#F4A4A3"}, # Regeneration (Recovery)
        #    {"name": "Zone 2", "lower": 124, "upper": 154, "color": "#EF7476"}, # Grundlagenausdauer (Endurance)
        #    {"name": "Zone 3", "lower": 154, "upper": 169, "color": "#EA4748"}, # Tempo (Aerob)
        #    {"name": "Zone 4", "lower": 169, "upper": 184, "color": "#E02628"}, # Schwelle (Threshold) (Anaerob)
        #    {"name": "Zone 5", "lower": 184, "upper": max_hr_estimated, "color": "#B71316"}, # Neuromuskulär (Neuromuskulär)
        #]
        zones = [
            {"name": "Zone 1", "lower": 0,   "upper": 124, "color": "#4A4A4A"}, # Regeneration (Recovery) (#111111 Transparent)
            {"name": "Zone 2", "lower": 124, "upper": 154, "color": "#87CEFA"}, # Grundlagenausdauer (Endurance)
            {"name": "Zone 3", "lower": 154, "upper": 169, "color": "#90EE90"}, # Tempo (Aerob)
            {"name": "Zone 4", "lower": 169, "upper": 184, "color": "#FFDAB9"}, # Schwelle (Threshold) (Anaerob)
            {"name": "Zone 5", "lower": 184, "upper": max_hr_estimated, "color": "#FFB6C1"}, # Neuromuskulär (Neuromuskulär)
        ]

        # Zeichne Zonen als Hintergrund (horizontale Rechtecke)
        for zone in zones:
            fig.add_hrect(
                y0=zone["lower"], y1=zone["upper"],
                fillcolor=zone["color"],
                opacity=0.15,
                line_width=0,
                annotation_text=zone["name"],  # optional: Name der Zone einblenden
                annotation_position="top left"
            )

    # Layout
    title_text = f'Herzfrequenz über die Zeit (geglättete)'
    if mean_hr > 0:
        title_text += f' - Ø {mean_hr:.0f} bpm (Range: {min_hr:.0f}-{max_hr:.0f})'

    fig.update_layout(
        title=dict(text=title_text, font=dict(size=16, color='white')),
        xaxis=dict(
            title='Zeit',
            tickformat='%H:%M',
            type='date'
        ),
        yaxis=dict(
            title='Herzfrequenz (bpm)',
            range=[80, 200]  # Statt rangemode='tozero'
        ),
        template='plotly_dark',
        paper_bgcolor='#1e1e1e',
        plot_bgcolor='#111111',
        font=dict(color='white'),
        margin=dict(l=40, r=40, t=50, b=40),
        height=400,
        uirevision='constant',  # Avoiding not needed Re-renderings
    )

    return fig











def create_pace_bars_plot(df, formatted_pace=None):
    # Ensure time column is datetime
    if not pd.api.types.is_datetime64_any_dtype(df['time']):
        df['time'] = pd.to_datetime(df['time'], errors='coerce')

    # Assign km segments
    df['km'] = df['cum_dist_km'].astype(int)

    # Time in seconds from start
    df['time_sec'] = (df['time'] - df['time'].iloc[0]).dt.total_seconds()

    # Step 3: Compute pace manually per km group
    df['km_start'] = np.nan
    df['segment_len'] = np.nan
    df['pace_min_per_km'] = np.nan

    for km_val, group in df.groupby('km'):
        dist_start = group['cum_dist_km'].iloc[0]
        dist_end = group['cum_dist_km'].iloc[-1]
        segment_len = dist_end - dist_start

        time_start = group['time_sec'].iloc[0]
        time_end = group['time_sec'].iloc[-1]
        elapsed_time_sec = time_end - time_start

        if segment_len > 0:
            pace_min_per_km = (elapsed_time_sec / 60) / segment_len
        else:
            pace_min_per_km = np.nan

        df.loc[group.index, 'km_start'] = km_val
        df.loc[group.index, 'segment_len'] = segment_len
        df.loc[group.index, 'pace_min_per_km'] = pace_min_per_km

    # Clean types
    df['km_start'] = df['km_start'].astype(int)
    df['segment_len'] = df['segment_len'].astype(float)
    df['pace_min_per_km'] = pd.to_numeric(df['pace_min_per_km'], errors='coerce')

    # Step 4: Create Plotly bar chart
    fig = go.Figure()
    fig.add_trace(go.Bar(
        x=df['km_start'],  # Mittig unter jeder Bar
        y=df['pace_min_per_km'],
        width=df['segment_len'],
        text=[f"{v:.1f} min/km" if pd.notnull(v) else "" for v in df['pace_min_per_km']],
        #textposition='outside',
        textposition='inside',
        marker_color='#125595',
        opacity=0.9,     # Transparenz
        name='Pace pro km',
        offset=0
    ))




    # #########
    # Calculate average pace if not provided from Info-Banner function
    total_distance_km = df['cum_dist_km'].iloc[-1]
    total_seconds = df['time_diff_sec'].iloc[-1]

    # Average pace (min/km) - KORRIGIERT
    if total_distance_km > 0:
        pace_sec_per_km = total_seconds / total_distance_km
        pace_min = int(pace_sec_per_km // 60)
        pace_sec = int(pace_sec_per_km % 60)
        formatted_pace = f"{pace_min}:{pace_sec:02d} min/km"
        # Numerischen Wert für die dash'ed Linie berechnen
        avg_pace_numeric = pace_sec_per_km / 60
    else:
        formatted_pace = "N/A"
        avg_pace_numeric = 0

    # Add horizontal dash'ed reference line (avg_pace_numeric)
    fig.add_shape(
        type='line',
        x0=0,  # Start bei 0
        x1=total_distance_km,  # Ende bei maximaler Distanz
        y0=avg_pace_numeric,
        y1=avg_pace_numeric,
        line=dict(color='gray', width=1, dash='dash'),
    )


    title_text = f'Tempo je Kilometer'
    title_text += f' - Ø {formatted_pace}'

    fig.update_layout(
        title=dict(text=title_text, font=dict(size=16, color='white')),
        xaxis_title='Distanz (km)',
        yaxis_title='Minuten pro km',
        barmode='overlay',
        bargap=0,
        bargroupgap=0,
        xaxis=dict(
            type='linear',
            range=[0, df['cum_dist_km'].iloc[-1]],
            tickmode='linear',
            dtick=1,
            showgrid=True
        ),
        template='plotly_dark',
        height=400,
        margin=dict(l=40, r=40, t=30, b=40),
        plot_bgcolor='#111111',
        paper_bgcolor='#1e1e1e',
        font=dict(color='white'),
        uirevision='constant',  # Avoiding not needed Re-renderings
    )
    return fig





# === App Setup ===
app = dash.Dash(__name__,
    suppress_callback_exceptions=True,         # Weniger Validierung
    compress=True,                             # Gzip-Kompression
    external_stylesheets=[dbc.themes.SLATE],
    title = "Jogging Dashboard"
)

app.layout = html.Div([
    html.H1("Jogging Dashboard", style={'textAlign': 'center'}),
    dcc.Store(id='stored-df'),

    html.Div([
        html.Label("Datei wählen:", style={'color': 'white'}),
        dcc.Dropdown(
            id='file-dropdown',
            options=list_files(),
            value=list_files()[0]['value'],  # immer gültig
            clearable=False,
            style={'width': '300px', 'color': 'black'}
        )
    ], style={'padding': '20px', 'backgroundColor': '#1e1e1e'}),

    html.Div(id='info-banner'),
    dcc.Graph(id='fig-map'),
    dcc.Graph(id='fig-elevation'),
    dcc.Graph(id='fig_deviation'),
    dcc.Graph(id='fig_speed'),
    dcc.Graph(id='fig_hr'),
    dcc.Graph(id='fig_pace_bars')
])


# === Callbacks ===
# Callback 1: Load GPX File and Store as JSON
@app.callback(
    Output('stored-df', 'data'),
    Input('file-dropdown', 'value')
)
def load_data(selected_file):                   # Dateipfad der ausgewählten Datei
    df = process_selected_file(selected_file)   # Verarbeitet diese Datei
    return df.to_json(date_format='iso', orient='split')

# Callback 2: Update All (static) Plots
@app.callback(
    Output('info-banner', 'children'),
    Output('fig-map', 'figure', allow_duplicate=True),
    Output('fig-elevation', 'figure'),
    Output('fig_deviation', 'figure'),
    Output('fig_speed', 'figure'),
    Output('fig_hr', 'figure'),
    Output('fig_pace_bars', 'figure'),
    Input('stored-df', 'data'),
    prevent_initial_call=True
)
def update_all_plots(json_data):
    df = pd.read_json(io.StringIO(json_data), orient='split')

    info = create_info_banner(df)
    fig_map = create_map_plot(df)
    fig_elev = create_elevation_plot(df)
    fig_dev = create_deviation_plot(df)
    fig_speed = create_speed_plot(df)
    fig_hr = create_heart_rate_plot(df)
    fig_pace = create_pace_bars_plot(df)

    return info, fig_map, fig_elev, fig_dev, fig_speed, fig_hr, fig_pace


# Callback 3: Hover → update only hover (dynamic) marker
@app.callback(
    Output('fig-map', 'figure'),
    Input('fig-elevation', 'hoverData'),
    State('fig-map', 'figure'),
    State('stored-df', 'data'),
    prevent_initial_call=True
)
def highlight_map(hoverData, fig_map, json_data):
    df = pd.read_json(io.StringIO(json_data), orient='split')

    if hoverData is not None:
        point_index = hoverData['points'][0]['pointIndex']
        lat, lon = df.iloc[point_index][['lat', 'lon']]

        # update the last trace (the empty Hovered Point trace)
        fig_map['data'][-1]['lat'] = [lat]
        fig_map['data'][-1]['lon'] = [lon]

    return fig_map

# === Run Server ===
if __name__ == '__main__':
    app.run(debug=True,
            port=8051,
            threaded=True,
            processes=1
    )

    
# NOTE:
#    Zusammenhang zwischen Pace und Geschwindigkeit
#     - Pace = Minuten pro Kilometer (z. B. 5:40/km)
#     - Geschwindigkeit = Kilometer pro Stunde (z. B. 10.71 km/h)
# 
