import pandas as pd
import streamlit as st
from pathlib import Path
from sklearn.ensemble import RandomForestClassifier, IsolationForest
from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix, classification_report, precision_score, recall_score, f1_score
import plotly.express as px

st.set_page_config(page_title='Network Anomaly Dashboard', page_icon="🛡️", layout="wide")
st.title("🛡️ Network Anomaly Dashboard")
st.info('Isolation Forest flags unusual synthetic network flows, not necessarily malicious attacks.')
st.caption("For offline education using synthetic data or separately authorized, sanitized feature CSVs. Not for security decisions.")
base=Path(__file__).parent
uploaded=st.file_uploader("Optional: upload sanitized feature CSV with same columns",type=["csv"])
try:
    data=pd.read_csv(uploaded if uploaded is not None else base/'sample_data.csv')
except Exception as exc:
    st.error(f"Could not read CSV: {exc}")
    st.stop()
features=['duration_s', 'bytes_sent', 'bytes_received', 'packets', 'dst_port', 'failed_connections', 'unique_destinations']
missing=[x for x in features if x not in data.columns]
if missing:
    st.error(f"Missing columns: {missing}")
    st.stop()
X=data[features].apply(pd.to_numeric,errors='coerce').replace([float('inf'),float('-inf')],float('nan'))
if X.isna().any().any():
    st.error("Features must be numeric, finite, and non-empty. Clean the CSV before continuing.")
    st.stop()
if len(X)<40:
    st.error("At least 40 rows are required for this educational exercise.")
    st.stop()
if 'anomaly'=='anomaly':
    model=IsolationForest(n_estimators=120,contamination=0.12,random_state=42)
    predicted=(model.fit_predict(X)==-1).astype(int)
    scores=-model.decision_function(X)
    st.metric("Unusual flows (illustrative)",int(predicted.sum()))
    st.caption("Anomaly scores are not calibrated attack probabilities. The model fits on the uploaded dataset.")
    data['anomaly_score']=scores
    data['flagged']=predicted
    if 'suspicious' in data.columns:
        y=pd.to_numeric(data['suspicious'],errors='coerce')
        if y.notna().all() and set(y.unique()).issubset({0,1}):
            st.write("Comparison against supplied labels (not independent validation):")
            st.write(classification_report(y,predicted,zero_division=0,output_dict=True))
else:
    if 'suspicious' not in data.columns:
        st.error("For supervised classification, include a 'suspicious' column with 0/1 labels.")
        st.stop()
    y=pd.to_numeric(data['suspicious'],errors='coerce')
    if y.isna().any() or set(y.unique())!={0,1} or y.value_counts().min()<3:
        st.error("The target must contain both 0 and 1, with at least three rows of each class.")
        st.stop()
    X_train,X_test,y_train,y_test=train_test_split(X,y,test_size=.25,random_state=42,stratify=y)
    model=RandomForestClassifier(n_estimators=120,max_depth=7,class_weight='balanced',random_state=42)
    model.fit(X_train,y_train)
    predicted=model.predict(X_test)
    prob=model.predict_proba(X_test)[:,1]
    a,b,c=st.columns(3)
    a.metric("Holdout precision",f"{precision_score(y_test,predicted,zero_division=0):.2f}")
    b.metric("Holdout recall",f"{recall_score(y_test,predicted,zero_division=0):.2f}")
    c.metric("Holdout F1",f"{f1_score(y_test,predicted,zero_division=0):.2f}")
    st.caption("Random stratified holdout is illustrative. Real network telemetry needs time/site-separated testing and leakage checks.")
    cm=confusion_matrix(y_test,predicted,labels=[0,1])
    st.plotly_chart(px.imshow(cm,text_auto=True,labels=dict(x='Predicted',y='Actual',color='Count'),x=['Normal','Flagged'],y=['Normal','Flagged'],title='Held-out confusion matrix'),use_container_width=True)
    importance=pd.DataFrame({'feature':features,'importance':model.feature_importances_}).sort_values('importance')
    st.plotly_chart(px.bar(importance,x='importance',y='feature',orientation='h',title='Model feature importance (not causal explanation)'),use_container_width=True)
    st.subheader("Inspect a held-out record")
    idx=st.selectbox('Sample row',range(len(X_test)),format_func=lambda i:f'Test record {i+1}')
    st.dataframe(X_test.iloc[[idx]],use_container_width=True)
    st.write(f"Flag probability: {prob[idx]:.1%} (not calibrated)")
st.subheader("Explore sample data")
st.dataframe(data.head(30),use_container_width=True)
st.download_button("Download current sample CSV",data.to_csv(index=False).encode('utf-8'),file_name='cybersecurity_features.csv',mime='text/csv')
st.warning("Never upload sensitive plant logs, credentials, IP inventories, executable malware or unapproved operational data.")
