use std::net::SocketAddr;
use std::sync::Arc;

use apalis::prelude::*;
use apalis_cron::CronStream;
use apalis_redis::{RedisConfig, RedisStorage};
use cron::Schedule;
use std::str::FromStr;
use axum::{middleware, routing::get, Router};
use tower_http::services::ServeDir;
use tower_http::trace::TraceLayer;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

use nexus::{
    auth::ory_auth_middleware,
    config::Config,
    db::Database,
    graphql::{build_schema, SchemaOptions},
    jobs::{
        DeliverNotificationJob, NotificationJobContext, ProcessEventJob,
        EventCleanupJob, IncompleteWorkReminderJob, NightlyAssignmentsJob,
        DELIVER_NOTIFICATION_QUEUE, PROCESS_EVENT_QUEUE,
    },
    routes::{self, graphql_handler, graphql_playground, MediaState},
    services::{GmailService, GoogleAuthService, GoogleCalendarService, JobQueue, S3Service, WaveService},
    signals,
};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Load .env file if present (for local development)
    dotenvy::dotenv().ok();

    // Initialize tracing
    tracing_subscriber::registry()
        .with(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| "nexus=debug,tower_http=debug".into()),
        )
        .with(tracing_subscriber::fmt::layer())
        .init();

    // Load configuration
    let config = Config::from_env().expect("Failed to load configuration");

    tracing::info!("Starting Nexus server on {}", config.bind_addr());

    // Connect to database
    let db = Database::connect().await?;

    // Spawn SIGHUP handler for credential refresh
    let db_clone = db.clone();
    tokio::spawn(async move {
        signals::handle_sighup(db_clone).await;
    });

    // Spawn file watcher as fallback for credential refresh
    // (handles cases where SIGHUP signaling fails)
    let db_clone = db.clone();
    tokio::spawn(async move {
        signals::watch_secrets_file(db_clone).await;
    });

    // Initialize Google services if configured
    let (calendar_service, gmail_service) = initialize_google_services(&config);

    // Initialize Wave service if configured
    let wave_service = initialize_wave_service(&config);

    // Initialize S3 service if configured
    let s3_service = initialize_s3_service(&config).await;

    // Initialize job queue
    let job_queue = initialize_job_queue(&config).await;

    // Build GraphQL schema with database and services
    let schema = build_schema(SchemaOptions {
        db: db.clone(),
        calendar_service,
        gmail_service: gmail_service.clone(),
        wave_service,
        s3_service: s3_service.clone(),
        job_queue: job_queue.clone(),
    });

    // Build authenticated routes (behind Ory middleware)
    // Playground (GET) needs Database for admin check
    // Handler (POST) needs Schema for query execution
    let graphql_routes = Router::new()
        .route("/graphql", get(graphql_playground))
        .with_state(db.clone())
        .merge(
            Router::new()
                .route("/graphql", axum::routing::post(graphql_handler))
                .with_state(schema),
        );

    // Build media proxy routes (if S3 is configured)
    let media_routes = if let Some(s3) = s3_service {
        let media_state = MediaState { s3 };
        Some(
            Router::new()
                .route("/api/media/{*path}", get(routes::get_media).head(routes::head_media))
                .with_state(media_state),
        )
    } else {
        None
    };

    // Combine authenticated routes
    let mut authenticated_routes = graphql_routes;
    if let Some(media) = media_routes {
        authenticated_routes = authenticated_routes.merge(media);
    }
    let authenticated_routes = authenticated_routes.layer(middleware::from_fn(ory_auth_middleware));

    // Build main router - health routes are public (no auth required)
    // Note: CORS is handled by Oathkeeper, not here
    let app = Router::new()
        .merge(routes::router(db.clone()))  // Public routes (/health, /health/ready)
        .nest_service("/static", ServeDir::new("static"))  // Static files (logo, etc.)
        .merge(authenticated_routes)
        .layer(TraceLayer::new_for_http());

    // Parse bind address
    let addr: SocketAddr = config.bind_addr().parse()?;

    // Start background workers if job queue is available
    if let Some(ref jq) = job_queue {
        let worker_handle = spawn_workers(
            db.clone(),
            gmail_service,
            jq.clone(),
            &config,
        ).await;

        // Spawn worker runner in background
        tokio::spawn(async move {
            if let Err(e) = worker_handle.await {
                tracing::error!("Worker error: {}", e);
            }
        });
    }

    // Start server
    tracing::info!("Nexus GraphQL API listening on http://{}", addr);
    let listener = tokio::net::TcpListener::bind(addr).await?;
    axum::serve(listener, app).await?;

    Ok(())
}

/// Initialize Google services if configured
fn initialize_google_services(
    config: &Config,
) -> (
    Option<Arc<GoogleCalendarService>>,
    Option<Arc<GmailService>>,
) {
    // Check if Google service account key is configured
    let service_account_key = match &config.google_service_account_key {
        Some(key) if !key.is_empty() => key,
        _ => {
            tracing::info!("Google services not configured (GOOGLE_SERVICE_ACCOUNT_KEY not set)");
            return (None, None);
        }
    };

    // Create shared auth service
    let auth = match GoogleAuthService::new(service_account_key) {
        Ok(auth) => Arc::new(auth),
        Err(e) => {
            tracing::warn!("Failed to initialize Google auth service: {}", e);
            return (None, None);
        }
    };

    // Initialize Calendar service if calendar ID is configured
    let calendar_service = config.google_calendar_id.as_ref().and_then(|calendar_id| {
        if calendar_id.is_empty() {
            tracing::info!("Google Calendar not configured (GOOGLE_CALENDAR_ID not set)");
            None
        } else {
            tracing::info!("Google Calendar service initialized for calendar: {}", calendar_id);
            Some(Arc::new(GoogleCalendarService::new(auth.clone(), calendar_id.clone(), calendar_id.clone())))
        }
    });

    // Initialize Gmail service if user email is configured
    let gmail_service = config.google_gmail_user.as_ref().and_then(|user_email| {
        if user_email.is_empty() {
            tracing::info!("Gmail service not configured (GOOGLE_GMAIL_USER not set)");
            None
        } else {
            tracing::info!("Gmail service initialized for user: {}", user_email);
            Some(Arc::new(GmailService::new(auth.clone(), user_email.clone())))
        }
    });

    (calendar_service, gmail_service)
}

/// Initialize Wave service if configured
fn initialize_wave_service(config: &Config) -> Option<Arc<WaveService>> {
    let access_token = match &config.wave_access_token {
        Some(token) if !token.is_empty() => token,
        _ => {
            tracing::info!("Wave service not configured (WAVE_ACCESS_TOKEN not set)");
            return None;
        }
    };

    let business_id = match &config.wave_business_id {
        Some(id) if !id.is_empty() => id,
        _ => {
            tracing::info!("Wave service not configured (WAVE_BUSINESS_ID not set)");
            return None;
        }
    };

    tracing::info!("Wave service initialized for business: {}", business_id);
    Some(Arc::new(WaveService::new(
        access_token.clone(),
        business_id.clone(),
    )))
}

/// Initialize S3 service if configured
async fn initialize_s3_service(config: &Config) -> Option<Arc<S3Service>> {
    if config.s3_endpoint.is_empty() || config.s3_access_key.is_empty() {
        tracing::info!("S3 service not configured (S3_ENDPOINT or S3_ACCESS_KEY not set)");
        return None;
    }

    match S3Service::new(config).await {
        Ok(s3) => {
            tracing::info!(
                "S3 service initialized (endpoint: {}, bucket: {})",
                config.s3_endpoint,
                config.s3_bucket
            );
            Some(Arc::new(s3))
        }
        Err(e) => {
            tracing::warn!("Failed to initialize S3 service: {}", e);
            None
        }
    }
}

/// Initialize job queue if Valkey is configured
async fn initialize_job_queue(config: &Config) -> Option<JobQueue> {
    if config.valkey_url.is_empty() {
        tracing::info!("Job queue not configured (VALKEY_URL not set)");
        return None;
    }

    match JobQueue::new(&config.valkey_url).await {
        Ok(queue) => {
            tracing::info!("Job queue initialized ({})", config.valkey_url);
            Some(queue)
        }
        Err(e) => {
            tracing::warn!("Failed to initialize job queue: {}", e);
            None
        }
    }
}

/// Context for scheduled jobs (simpler than NotificationJobContext)
struct ScheduledJobContext {
    db: Database,
    gmail_service: Option<Arc<GmailService>>,
}

/// Spawn background workers for notification processing and scheduled jobs
async fn spawn_workers(
    db: Database,
    gmail_service: Option<Arc<GmailService>>,
    job_queue: JobQueue,
    config: &Config,
) -> tokio::task::JoinHandle<()> {
    let ctx = Arc::new(NotificationJobContext::new(db.clone(), gmail_service.clone(), job_queue.clone()));
    let scheduled_ctx = Arc::new(ScheduledJobContext {
        db: db.clone(),
        gmail_service: gmail_service.clone(),
    });

    // Connect to Redis for workers
    let conn = apalis_redis::connect(config.valkey_url.clone())
        .await
        .expect("Failed to connect to Valkey for workers");

    // Create storage for process event jobs
    let event_storage: RedisStorage<ProcessEventJob> = RedisStorage::new_with_config(
        conn.clone(),
        RedisConfig::default().set_namespace(PROCESS_EVENT_QUEUE),
    );

    // Create storage for delivery jobs
    let delivery_storage: RedisStorage<DeliverNotificationJob> = RedisStorage::new_with_config(
        conn,
        RedisConfig::default().set_namespace(DELIVER_NOTIFICATION_QUEUE),
    );

    // Create cron schedules (UTC times)
    // Format: sec min hour day-of-month month day-of-week year
    // 2 AM Eastern = 7 AM UTC (EST) / 6 AM UTC (EDT)
    // 8 AM Eastern = 1 PM UTC (EST) / 12 PM UTC (EDT)
    // 6 PM Eastern = 11 PM UTC (EST) / 10 PM UTC (EDT)
    // Using EST offsets (5 hours) - will be off by 1 hour during DST
    let cleanup_schedule = Schedule::from_str("0 0 7 * * * *").expect("Invalid cleanup schedule");
    let reminder_schedule = Schedule::from_str("0 0 13 * * * *").expect("Invalid reminder schedule");
    let assignments_schedule = Schedule::from_str("0 0 23 * * * *").expect("Invalid assignments schedule");

    // Create worker functions
    let ctx_event = Arc::clone(&ctx);
    let ctx_delivery = Arc::clone(&ctx);
    let ctx_cleanup = Arc::clone(&scheduled_ctx);
    let ctx_reminder = Arc::clone(&scheduled_ctx);
    let ctx_assignments = Arc::clone(&scheduled_ctx);

    // Build and run the monitor with workers
    tokio::spawn(async move {
        let monitor = Monitor::new()
            // On-demand notification workers
            .register(move |_instance| {
                WorkerBuilder::new(PROCESS_EVENT_QUEUE)
                    .backend(event_storage.clone())
                    .data(ctx_event.clone())
                    .build(|job: ProcessEventJob, ctx: Data<Arc<NotificationJobContext>>| async move {
                        job.execute(&ctx).await
                    })
            })
            .register(move |_instance| {
                WorkerBuilder::new(DELIVER_NOTIFICATION_QUEUE)
                    .backend(delivery_storage.clone())
                    .data(ctx_delivery.clone())
                    .build(|job: DeliverNotificationJob, ctx: Data<Arc<NotificationJobContext>>| async move {
                        job.execute(&ctx).await
                    })
            })
            // Scheduled cron workers
            .register({
                let schedule = cleanup_schedule.clone();
                let ctx = ctx_cleanup.clone();
                move |_instance| {
                    let ctx = ctx.clone();
                    WorkerBuilder::new("event-cleanup")
                        .backend(CronStream::new(schedule.clone()))
                        .data(ctx)
                        .build(|_tick, ctx: Data<Arc<ScheduledJobContext>>| async move {
                            tracing::info!("Running event cleanup job");
                            let pool = ctx.db.pool().await;
                            match EventCleanupJob::execute(&*pool).await {
                                Ok(result) => {
                                    tracing::info!(
                                        deleted = result.deleted_count,
                                        cutoff = %result.cutoff_date,
                                        "Event cleanup completed"
                                    );
                                    Ok(())
                                }
                                Err(e) => {
                                    tracing::error!("Event cleanup failed: {}", e);
                                    Err(e)
                                }
                            }
                        })
                }
            })
            .register({
                let schedule = reminder_schedule.clone();
                let ctx = ctx_reminder.clone();
                move |_instance| {
                    let ctx = ctx.clone();
                    WorkerBuilder::new("incomplete-work-reminder")
                        .backend(CronStream::new(schedule.clone()))
                        .data(ctx)
                        .build(|_tick, ctx: Data<Arc<ScheduledJobContext>>| async move {
                            tracing::info!("Running incomplete work reminder job");
                            let pool = ctx.db.pool().await;
                            match IncompleteWorkReminderJob::execute(
                                &*pool,
                                ctx.gmail_service.as_ref().map(|s| s.as_ref()),
                            ).await {
                                Ok(result) => {
                                    tracing::info!(
                                        emails = result.emails_sent,
                                        services = result.services_count,
                                        projects = result.projects_count,
                                        "Incomplete work reminder completed"
                                    );
                                    Ok(())
                                }
                                Err(e) => {
                                    tracing::error!("Incomplete work reminder failed: {}", e);
                                    Err(e)
                                }
                            }
                        })
                }
            })
            .register({
                let schedule = assignments_schedule.clone();
                let ctx = ctx_assignments.clone();
                move |_instance| {
                    let ctx = ctx.clone();
                    WorkerBuilder::new("nightly-assignments")
                        .backend(CronStream::new(schedule.clone()))
                        .data(ctx)
                        .build(|_tick, ctx: Data<Arc<ScheduledJobContext>>| async move {
                            tracing::info!("Running nightly assignments job");
                            let pool = ctx.db.pool().await;
                            match NightlyAssignmentsJob::execute(
                                &*pool,
                                ctx.gmail_service.as_ref().map(|s| s.as_ref()),
                            ).await {
                                Ok(result) => {
                                    tracing::info!(
                                        emails = result.emails_sent,
                                        services = result.services_count,
                                        projects = result.projects_count,
                                        "Nightly assignments completed"
                                    );
                                    Ok(())
                                }
                                Err(e) => {
                                    tracing::error!("Nightly assignments failed: {}", e);
                                    Err(e)
                                }
                            }
                        })
                }
            });

        tracing::info!("Starting notification and scheduled workers");
        if let Err(e) = monitor.run().await {
            tracing::error!("Worker monitor error: {}", e);
        }
    })
}
