pub mod agents;
pub mod agents_sse;
pub mod anthropic;
pub mod assets;
pub mod bot_command;
pub mod chat;
pub mod context;
pub mod health;
pub mod io;
pub mod mcp;
pub mod model;
pub mod oauth;
pub mod settings;
pub mod workflow;

pub mod project;
pub mod wizard;
pub mod ws;

use agents::AgentsApi;
use anthropic::AnthropicApi;
use bot_command::BotCommandApi;
use chat::ChatApi;
use context::AppContext;
use health::HealthApi;
use io::IoApi;
use model::ModelApi;
use poem::EndpointExt;
use poem::{Route, get, post};
use poem_openapi::OpenApiService;
use project::ProjectApi;
use settings::SettingsApi;
use std::path::{Path, PathBuf};
use std::sync::Arc;

use crate::chat::transport::slack::SlackWebhookContext;
use crate::chat::transport::whatsapp::WhatsAppWebhookContext;

const DEFAULT_PORT: u16 = 3001;

pub fn parse_port(value: Option<String>) -> u16 {
    value
        .and_then(|v| v.parse::<u16>().ok())
        .unwrap_or(DEFAULT_PORT)
}

pub fn resolve_port() -> u16 {
    parse_port(std::env::var("STORKIT_PORT").ok())
}

pub fn write_port_file(dir: &Path, port: u16) -> Option<PathBuf> {
    let path = dir.join(".storkit_port");
    std::fs::write(&path, port.to_string()).ok()?;
    Some(path)
}

pub fn remove_port_file(path: &Path) {
    let _ = std::fs::remove_file(path);
}

pub fn build_routes(
    ctx: AppContext,
    whatsapp_ctx: Option<Arc<WhatsAppWebhookContext>>,
    slack_ctx: Option<Arc<SlackWebhookContext>>,
) -> impl poem::Endpoint {
    let ctx_arc = std::sync::Arc::new(ctx);

    let (api_service, docs_service) = build_openapi_service(ctx_arc.clone());

    let oauth_state = Arc::new(oauth::OAuthState::new(resolve_port()));

    let mut route = Route::new()
        .nest("/api", api_service)
        .nest("/docs", docs_service.swagger_ui())
        .at("/ws", get(ws::ws_handler))
        .at(
            "/agents/:story_id/:agent_name/stream",
            get(agents_sse::agent_stream),
        )
        .at(
            "/mcp",
            post(mcp::mcp_post_handler).get(mcp::mcp_get_handler),
        )
        .at("/health", get(health::health))
        .at(
            "/oauth/authorize",
            get(oauth::oauth_authorize).data(oauth_state.clone()),
        )
        .at(
            "/callback",
            get(oauth::oauth_callback).data(oauth_state.clone()),
        )
        .at(
            "/oauth/status",
            get(oauth::oauth_status),
        )
        .at("/assets/*path", get(assets::embedded_asset))
        .at("/", get(assets::embedded_index))
        .at("/*path", get(assets::embedded_file));

    if let Some(wa_ctx) = whatsapp_ctx {
        route = route.at(
            "/webhook/whatsapp",
            get(crate::chat::transport::whatsapp::webhook_verify)
                .post(crate::chat::transport::whatsapp::webhook_receive)
                .data(wa_ctx),
        );
    }

    if let Some(sl_ctx) = slack_ctx {
        route = route
            .at(
                "/webhook/slack",
                post(crate::chat::transport::slack::webhook_receive).data(sl_ctx.clone()),
            )
            .at(
                "/webhook/slack/command",
                post(crate::chat::transport::slack::slash_command_receive).data(sl_ctx),
            );
    }

    route.data(ctx_arc)
}

type ApiTuple = (
    ProjectApi,
    ModelApi,
    AnthropicApi,
    IoApi,
    ChatApi,
    AgentsApi,
    SettingsApi,
    HealthApi,
    BotCommandApi,
    wizard::WizardApi,
);

type ApiService = OpenApiService<ApiTuple, ()>;

/// All HTTP methods are documented by OpenAPI at /docs
pub fn build_openapi_service(ctx: Arc<AppContext>) -> (ApiService, ApiService) {
    let api = (
        ProjectApi { ctx: ctx.clone() },
        ModelApi { ctx: ctx.clone() },
        AnthropicApi::new(ctx.clone()),
        IoApi { ctx: ctx.clone() },
        ChatApi { ctx: ctx.clone() },
        AgentsApi { ctx: ctx.clone() },
        SettingsApi { ctx: ctx.clone() },
        HealthApi,
        BotCommandApi { ctx: ctx.clone() },
        wizard::WizardApi { ctx: ctx.clone() },
    );

    let api_service =
        OpenApiService::new(api, "Storkit API", "1.0").server("http://127.0.0.1:3001/api");

    let docs_api = (
        ProjectApi { ctx: ctx.clone() },
        ModelApi { ctx: ctx.clone() },
        AnthropicApi::new(ctx.clone()),
        IoApi { ctx: ctx.clone() },
        ChatApi { ctx: ctx.clone() },
        AgentsApi { ctx: ctx.clone() },
        SettingsApi { ctx: ctx.clone() },
        HealthApi,
        BotCommandApi { ctx: ctx.clone() },
        wizard::WizardApi { ctx },
    );

    let docs_service =
        OpenApiService::new(docs_api, "Storkit API", "1.0").server("http://127.0.0.1:3001/api");

    (api_service, docs_service)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_port_defaults_to_3001() {
        assert_eq!(parse_port(None), 3001);
    }

    #[test]
    fn parse_port_reads_valid_value() {
        assert_eq!(parse_port(Some("4200".to_string())), 4200);
    }

    #[test]
    fn parse_port_ignores_invalid_value() {
        assert_eq!(parse_port(Some("not_a_number".to_string())), 3001);
    }

    #[test]
    fn write_and_remove_port_file() {
        let tmp = tempfile::tempdir().unwrap();

        let path = write_port_file(tmp.path(), 4567).expect("should write port file");
        assert_eq!(std::fs::read_to_string(&path).unwrap(), "4567");

        remove_port_file(&path);
        assert!(!path.exists());
    }

    #[test]
    fn write_port_file_returns_none_on_nonexistent_dir() {
        let bad = std::path::Path::new("/this_dir_does_not_exist_storykit_test_xyz");
        assert!(write_port_file(bad, 1234).is_none());
    }

    #[test]
    fn remove_port_file_does_not_panic_for_missing_file() {
        let path = std::path::Path::new("/tmp/nonexistent_storykit_port_test_xyz_999");
        remove_port_file(path);
    }

    #[test]
    fn resolve_port_returns_a_valid_port() {
        // Exercises the resolve_port code path (reads STORKIT_PORT env var or defaults).
        let port = resolve_port();
        assert!(port > 0);
    }

    #[test]
    fn build_openapi_service_constructs_without_panic() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = Arc::new(context::AppContext::new_test(tmp.path().to_path_buf()));
        let (_api_service, _docs_service) = build_openapi_service(ctx);
    }

    #[test]
    fn build_routes_constructs_without_panic() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = context::AppContext::new_test(tmp.path().to_path_buf());
        let _endpoint = build_routes(ctx, None, None);
    }
}
