import dash
from dash import Dash, html, dcc, Input, Output, State
import dash_bootstrap_components as dbc
import pandas as pd
from sklearn.metrics.pairwise import cosine_similarity
from sklearn.preprocessing import MinMaxScaler
import plotly.graph_objects as go
import plotly.express as px
import numpy as np
# Initialize the Dash app with a modern Bootstrap theme
# Add suppress_callback_exceptions=True to avoid errors with dynamic components
app = Dash(__name__, external_stylesheets=[dbc.themes.UNITED], suppress_callback_exceptions=True)
# Load the GroceryDB_foods dataset and remove rows with NaN
df = pd.read_csv("GroceryDB_food.csv").dropna()
df['name'] = df.name.str.split(',', expand=True)[0]
df = df.reset_index(drop=True)
# Select relevant columns
nutritional_columns = [
    'Protein', 'Total Fat', 'Carbohydrate', 'Sugars, total',
    'Fiber, total dietary', 'Calcium', 'Iron', 'Sodium', 'Vitamin C',
    'Cholesterol', 'Fatty acids, total saturated', 'Total Vitamin A'
]
# Scale the dataset
scaler = MinMaxScaler()
df_scaled = pd.DataFrame(scaler.fit_transform(df[nutritional_columns]), columns=nutritional_columns)
# Custom style for the entire application
CONTENT_STYLE = {
    "marginLeft": "1rem",
    "marginRight": "1rem",
    "padding": "1rem 1rem",
}
# Colors in RGBA format to use transparency correctly
radar_colors = [
    'rgba(31, 119, 180, 0.8)',   # blue
    'rgba(255, 127, 14, 0.8)',   # orange
    'rgba(44, 160, 44, 0.8)',    # green
    'rgba(214, 39, 40, 0.8)',    # red
    'rgba(148, 103, 189, 0.8)',  # purple
    'rgba(140, 86, 75, 0.8)',    # brown
    'rgba(227, 119, 194, 0.8)',  # pink
    'rgba(188, 189, 34, 0.8)',   # olive
    'rgba(23, 190, 207, 0.8)'    # cyan
]
transparent_radar_colors = [
    'rgba(31, 119, 180, 0.2)',   # transparent blue
    'rgba(255, 127, 14, 0.2)',   # transparent orange
    'rgba(44, 160, 44, 0.2)',    # transparent green
    'rgba(214, 39, 40, 0.2)',    # transparent red
    'rgba(148, 103, 189, 0.2)',  # transparent purple
    'rgba(140, 86, 75, 0.2)',    # transparent brown
    'rgba(227, 119, 194, 0.2)',  # transparent pink
    'rgba(188, 189, 34, 0.2)',   # transparent olive
    'rgba(23, 190, 207, 0.2)'    # transparent cyan
]
app.title = "Smart Food Recommender"
# Modernized and reorganized application layout
app.layout = dbc.Container([
    # Header with colored background and centered text
    dbc.Row([
        dbc.Col([
            html.Div([
                html.H3("Smart Food Recommender", className="display-4 text-white"),
                html.P("Find similar products based on their nutritional profile", className="lead text-white")
            ], className="p-2 bg-secondary rounded-3 text-center my-4")
        ])
    ]),
    # Product selection section (now in a separate row)
    dbc.Row([
        dbc.Col([
            dbc.Button("Select Product", id="open-modal-btn", color="primary", className="mb-3"),
            html.Div(id="product-alert"),  # Alert display here
        ], className="text-center")
    ]),
    # Modal for product selection
    dbc.Modal([
        dbc.ModalHeader("Select a product"),
        dbc.ModalBody([
            dcc.Dropdown(
                id="product-dropdown",
                options=[{"label": row['name'], "value": idx} for idx, row in df.iterrows()],
                placeholder="Search and select product...",
                className="mb-4",
                style={"fontSize": "14px"}  
            ),
        ]),
        dbc.ModalFooter(
            dbc.Button("Close", id="close-modal-btn", className="ms-auto", color="secondary")
        ),
    ], id="modal", size="lg"),
    # Section to display the currently selected product
    dbc.Row([
        dbc.Col([
            html.Div(id="selected-product", className="text-center mb-3 fw-bold fs-4"),
        ])
    ]),
    # Visualization options (RadioItems)
    dbc.Row([
        dbc.Col([
            dbc.Card([
                dbc.CardBody([
                    dcc.RadioItems(
                        id="visualization-options",
                        options=[
                            {"label": "Details and Recommendations", "value": "details-recommendations"},
                            {"label": "Nutritional Comparison", "value": "nutritional-comparison"}
                        ],
                        value="details-recommendations",
                        inline=True,
                        className="mb-2",
                        inputClassName="me-2",
                        labelClassName="me-3"
                    )
                ])
            ], className="shadow-sm mb-4")
        ])
    ]),
    # Container for the selected visualization
    html.Div(id="dynamic-content"),
    # IMPORTANT: Add empty components with correct IDs to avoid callback errors
    # These elements will be initially empty, but are necessary for the callbacks to function
    html.Div(id="product-details", style={"display": "none"}),
    html.Div(id="recommendations", style={"display": "none"}),
    dcc.Graph(id="radar-chart", style={"display": "none"}),
    # Footer
    dbc.Row([
        dbc.Col([
            html.Footer([
                html.P("© 2025 Smart Food Recommender", className="text-center text-muted")
            ], className="mt-5 pt-4 border-top")
        ])
    ])
], fluid=True, #style=CONTENT_STYLE
                          )
# Callback to open and close the modal
@app.callback(
    Output("modal", "is_open"),
    [Input("open-modal-btn", "n_clicks"), Input("close-modal-btn", "n_clicks")],
    [State("modal", "is_open")],
)
def toggle_modal(n1, n2, is_open):
    if n1 or n2:
        return not is_open
    return is_open
# Callback to update dynamic content based on the selected option
@app.callback(
    Output("dynamic-content", "children"),
    [Input("visualization-options", "value"),
     Input("product-dropdown", "value")]
)
def update_content(selected_option, product_id):
    if selected_option == "details-recommendations":
        return dbc.Row([
            # Left column - Product details
            dbc.Col([
                dbc.Card([
                    dbc.CardHeader([
                        html.H6("Product Details", className="card-title mb-0"),
                    ], className="bg-light"),
                    dbc.CardBody([
                        html.Div(id="product-details-container"),  # Container for details
                    ])
                ], className="h-100 shadow-sm")
            ], md=5),
            # Right column - Recommendations
            dbc.Col([
                dbc.Card([
                    dbc.CardHeader([
                        html.H6("Recommended Products", className="card-title mb-0"),
                    ], className="bg-light"),
                    dbc.CardBody([
                        dbc.Row(id="recommendations-container", className="row-cols-1 row-cols-md-2 g-4")
                    ])
                ], className="h-100 shadow-sm")
            ], md=7)
        ])
    else:
        return dbc.Row([
            dbc.Col([
                dbc.Card([
                    dbc.CardHeader([
                        html.H4("Nutritional Comparison", className="card-title mb-0"),
                    ], className="bg-light"),
                    dbc.CardBody([
                        dcc.Graph(id="radar-chart-container")
                    ])
                ], className="shadow-sm")
            ])
        ])
# Callback to display the selected product
@app.callback(
    Output("selected-product", "children"),
    [Input("product-dropdown", "value")]
)
def display_selected_product(product_id):
    if product_id is None:
        return "No product selected"
    try:
        product = df.loc[int(product_id)]
        return f"Selected product: {product['name']}"
    except Exception:
        return "Error loading product"
# Callback to display details, recommendations, charts and alerts
@app.callback(
    [Output("product-details", "children"),
     Output("recommendations", "children"),
     Output("radar-chart", "figure"),
     Output("product-alert", "children")],
    [Input("product-dropdown", "value")]
)
def update_recommendations(product_id):
    if product_id is None:
        # Return initial states when no product is selected
        return html.Div([
            html.P("Select a product to view its details and recommendations.", className="text-muted fst-italic")
        ]), [], go.Figure(layout=dict(title="Select a product to view the comparison")), None
    try:
        # Details of the selected product
        product = df.loc[int(product_id)]
        # Create nutritional information table
        nutrients_values = {
            'Nutrient': nutritional_columns,
            'Value': [round(product[col], 2) for col in nutritional_columns]
        }
        nutritional_table = dbc.Table.from_dataframe(
            pd.DataFrame(nutrients_values),
            striped=True,
            bordered=False,
            hover=True,
            responsive=True,
            className="mt-3"
        )
        details = html.Div([
            html.Div([
                html.H6(product['name'], className="mb-2"),
                html.P(f"Brand: {product['brand']}", className="mb-1 fs-5"),
                html.P([
                    html.Span("Price: ", className="fw-bold"),
                    html.Span(f"US${product['price']:.2f}", className="text-primary fs-4")
                ], className="mb-3"),
            ]),
            html.Hr(),
            html.H5("Nutritional Information", className="mt-4 mb-3"),
            nutritional_table
        ])
        # Evaluate product conditions for the alert
        unmet_criteria = 0
        criteria_text = []
        if product['Total Fat'] > 20:
            unmet_criteria += 1
            criteria_text.append("high in fats")
        if product['Sugars, total'] > 15:
            unmet_criteria += 1
            criteria_text.append("high in sugars")
        if product['Sodium'] > 500:
            unmet_criteria += 1
            criteria_text.append("high in sodium")
        # Determine alert color and message
        if unmet_criteria == 0:
            alert_color = "success"
            alert_message = "This product has a favorable nutritional profile."
            icon = "✓"
        elif unmet_criteria == 1:
            alert_color = "warning"
            alert_message = f"Caution: Product {criteria_text[0]}."
            icon = "⚠️"
        elif unmet_criteria == 2:
            alert_color = "warning"
            alert_message = f"Attention: Product {' and '.join(criteria_text)}."
            icon = "⚠️"
        else:
            alert_color = "danger"
            alert_message = "Not recommended: Product with multiple unfavorable nutritional factors."
            icon = "✗"
        alert = dbc.Alert([
            html.Span(icon, className="me-2"),
            html.Span(alert_message)
        ], color=alert_color, className="mt-3")
        # Calculate similarities for recommendations
        reference_product = df_scaled.iloc[[int(product_id)]]
        similarities = cosine_similarity(reference_product, df_scaled)
        df_temp = df.copy()
        df_temp['similarity'] = similarities[0]
        # Generate improved recommendation cards
        recommended = df_temp.sort_values(by='similarity', ascending=False).iloc[1:7]  # Show 6 recommendations
        cards = []
        for idx, row in recommended.iterrows():
            # Evaluate conditions for this recommended product
            unmet_conditions = 0
            if row['Total Fat'] > 20: unmet_conditions += 1
            if row['Sugars, total'] > 15: unmet_conditions += 1
            if row['Sodium'] > 500: unmet_conditions += 1
            # Determine card border color based on nutritional profile
            if unmet_conditions == 0:
                border_color = "success"
            elif unmet_conditions == 1:
                border_color = "warning"
            elif unmet_conditions == 2:
                border_color = "warning"
            else:
                border_color = "danger"
            # Calculate similarity percentage to display
            similarity_percentage = int(row['similarity'] * 100)
            card = dbc.Col(
                dbc.Card([
                    dbc.CardHeader([
                        html.Div([
                            html.Span(f"{similarity_percentage}% similar",
                                      className="badge bg-primary float-end")
                        ])
                    ], className=f"border-{border_color}"),
                    dbc.CardBody([
                        html.H6(row['name'], className="card-title text-truncate",
                                title=row['name']),
                        html.P(f"Brand: {row['brand']}", className="card-text"),
                        html.Div([
                            html.Span("US$", className="text-muted me-1"),
                            html.Span(f"{row['price']:.2f}", className="fw-bold fs-5")
                        ], className="mt-2")
                    ])
                ], className=f"h-100 shadow-sm border-{border_color}")
            )
            cards.append(card)
        # Create improved radar chart with correct RGBA format
        fig = go.Figure()
        # Add selected product with thicker line
        fig.add_trace(go.Scatterpolar(
            r=[product[col] for col in nutritional_columns],
            theta=nutritional_columns,
            fill='toself',
            name=product['name'],
            line=dict(color=radar_colors[0], width=3),
            fillcolor=transparent_radar_colors[0]  # Use RGBA color with transparency
        ))
        # Add all recommended products to the chart (limiting to available colors)
        max_products = min(len(recommended), len(radar_colors) - 1)  # -1 because we already used the first color
        for i, (_, row) in enumerate(recommended.iloc[:max_products].iterrows()):
            if i < len(radar_colors) - 1:  # Make sure we don't exceed available colors
                fig.add_trace(go.Scatterpolar(
                    r=[row[col] for col in nutritional_columns],
                    theta=nutritional_columns,
                    fill='toself',
                    name=row['name'],
                    line=dict(color=radar_colors[i + 1]),
                    fillcolor=transparent_radar_colors[i + 1]  # Use RGBA color with transparency
                ))
        # Improve chart design
        fig.update_layout(
            polar=dict(
                radialaxis=dict(
                    visible=True,
                    showticklabels=True,
                    gridcolor="rgba(0,0,0,0.1)",
                ),
                angularaxis=dict(
                    gridcolor="rgba(0,0,0,0.1)",
                ),
                bgcolor="rgba(0,0,0,0.02)"
            ),
            showlegend=True,
            legend=dict(
                orientation="h",
                yanchor="bottom",
                y=-0.3,
                xanchor="center",
                x=0.5
            ),
            title={
                'text': "Nutritional Profile Comparison",
                'y': 0.95,
                'x': 0.5,
                'xanchor': 'center',
                'yanchor': 'top'
            },
            margin=dict(l=80, r=80, t=100, b=100),
            paper_bgcolor='rgba(0,0,0,0)',
            plot_bgcolor='rgba(0,0,0,0)',
            height=600  # Fixed height for better visualization
        )
        return details, cards, fig, alert
    except Exception as e:
        # Error handling for debugging
        print(f"Error in callback: {e}")
        return html.Div([
            html.P(f"Error: Could not process request. {str(e)}", className="text-danger")
        ]), [], go.Figure(), dbc.Alert("Error processing data", color="danger")
# Additional callbacks to update dynamic containers
@app.callback(
    Output("product-details-container", "children"),
    [Input("product-details", "children")]
)
def update_details_container(details):
    return details
@app.callback(
    Output("recommendations-container", "children"),
    [Input("recommendations", "children")]
)
def update_recommendations_container(recommendations):
    return recommendations
@app.callback(
    Output("radar-chart-container", "figure"),
    [Input("radar-chart", "figure")]
)
def update_chart_container(figure):
    return figure