//! `roundabout` — a macro-driven, CQRS-style ORM runtime.
//!
//! The [`entity`] macro emits a JSON manifest at build time; this crate reads
//! that manifest and builds the HTTP + WebSocket API:
//!
//! * **Reads** are served from Postgres (the query side).
//! * **Writes** are staged in SQLite and acknowledged immediately (the command
//!   side); a background worker migrates them to Postgres and then broadcasts a
//!   WebSocket alert for any changed `Live<T>` field.
//!
//! ```no_run
//! # async fn run() {
//! let app = roundabout::App::from_schema_dir("./schema").await;
//! app.serve("127.0.0.1:8080").await;
//! # }
//! ```

pub mod db;
pub mod docs;
pub mod manifest;
pub mod migrate;
pub mod openapi;
pub mod router;
pub mod state;
pub mod sync;
pub mod types;

/// The `#[entity]` attribute macro — re-exported so a single `roundabout`
/// dependency provides both the runtime and the schema-generating macro.
pub use roundabout_orm_macros::entity;
/// The shared schema model (`EntityDef`, `FieldDef`, …) for advanced use.
pub use roundabout_orm_schema as schema;

use state::{build_hub, AppState};
use std::sync::Arc;

/// Connection-string env vars, with sensible local defaults.
const PG_ENV: &str = "ROUND_ABOUT_PG";
const SQLITE_ENV: &str = "ROUND_ABOUT_SQLITE";
const MIGRATIONS_ENV: &str = "ROUND_ABOUT_MIGRATIONS_DIR";
const MIGRATE_ENV: &str = "ROUND_ABOUT_MIGRATE";
const DEFAULT_PG: &str = "host=localhost user=postgres password=pw dbname=postgres";
const DEFAULT_SQLITE: &str = "roundabout_orm_outbox.db";
const DEFAULT_MIGRATIONS: &str = "./migrations";

/// Application configuration: supply explicitly or read from environment.
pub struct Config {
    /// Postgres connection string.
    pub pg_conn: String,
    /// SQLite staging DB path.
    pub sqlite_path: String,
    /// Migrations directory.
    pub migrations_dir: String,
    /// Whether to run migrations on startup.
    pub run_migrations: bool,
}

impl Config {
    /// Build a `Config` from environment variables (falling back to defaults).
    pub fn from_env() -> Self {
        let pg_conn = std::env::var(PG_ENV).unwrap_or_else(|_| DEFAULT_PG.to_string());
        let sqlite_path = std::env::var(SQLITE_ENV).unwrap_or_else(|_| DEFAULT_SQLITE.to_string());
        let migrations_dir = std::env::var(MIGRATIONS_ENV).unwrap_or_else(|_| DEFAULT_MIGRATIONS.to_string());
        let run_migrations = std::env::var(MIGRATE_ENV).map(|v| v != "off").unwrap_or(true);
        Self {
            pg_conn,
            sqlite_path,
            migrations_dir,
            run_migrations,
        }
    }
}

/// A configured application: loaded schema, database handles, and a running
/// sync worker. Call [`App::serve`] to start the HTTP server.
pub struct App {
    state: AppState,
}

impl App {
    /// Load the manifest from `dir` using environment-derived `Config`.
    pub async fn from_schema_dir(dir: impl AsRef<std::path::Path>) -> Self {
        Self::from_schema_dir_with_config(dir, Config::from_env()).await
    }

    /// Load the manifest from `dir` using an explicit `Config`.
    pub async fn from_schema_dir_with_config(
        dir: impl AsRef<std::path::Path>,
        cfg: Config,
    ) -> Self {
        let manifest = Arc::new(manifest::load_dir(dir));
        println!(
            "[roundabout_orm] loaded {} entit{}",
            manifest.len(),
            if manifest.len() == 1 { "y" } else { "ies" }
        );

        let pg = db::postgres::build_pool(&cfg.pg_conn).expect("failed to build Postgres pool");

        if cfg.run_migrations {
            migrate::generate(&manifest, &cfg.migrations_dir);
            match migrate::run_pending(&pg, &cfg.migrations_dir).await {
                Ok(n) => println!("[roundabout_orm] migrations up to date ({n} applied)"),
                Err(e) => eprintln!("[roundabout_orm] migration run failed (continuing): {e}"),
            }
        }

        let sqlite = db::sqlite::open(&cfg.sqlite_path).expect("failed to open SQLite staging db");

        let ws_hub = build_hub(&manifest);
        let openapi = Arc::new(openapi::build_spec(&manifest));

        let state = AppState {
            pg,
            sqlite,
            manifest,
            ws_hub,
            openapi,
        };

        sync::spawn(state.clone());

        Self { state }
    }

    /// Borrow the shared state (useful for tests and embedding).
    pub fn state(&self) -> &AppState {
        &self.state
    }

    /// Build the axum router without binding a socket.
    pub fn router(&self) -> axum::Router {
        router::build(self.state.clone())
    }

    /// Bind `addr` and serve until the process is stopped.
    pub async fn serve(self, addr: &str) {
        let app = router::build(self.state);
        let listener = tokio::net::TcpListener::bind(addr)
            .await
            .unwrap_or_else(|e| panic!("failed to bind {addr}: {e}"));
        println!("[roundabout_orm] listening on http://{addr}");
        axum::serve(listener, app).await.expect("server error");
    }
}
