use std::io;
use std::net::SocketAddr;

use cartel_redis::{DEFAULT_BACKOFF, Ops, Session};
use dope::fiber::Holding;
use dope::launcher::{Ctx, Launcher};
use dope::manifold::connector::Connector;
use dope::manifold::connector::source::Static;
use dope::manifold::env::Bundle;
use dope::manifold::listener::{Listener, config};
use dope::runtime::profile::Throughput;
use dope::transport::Tcp;
use dope::wire::Identity;
use dope::{DriverConfig, Executor};
use dope_extra::Trigger;
use http::StatusCode;
use o3::buffer::Owned;
use sark::date::{DateHost, Updater};
use sark::timer::TimerHost;
use sark::{Application, ServerCfg};
use sark_core::http::LocalFrameBytes;

type Env = Bundle<Tcp, Identity, Throughput>;
type RedisConnector = Connector<0, Session, Static<Tcp>, Env>;

#[derive(Clone)]
struct AppState<'d> {
    redis: Holding<'d, RedisConnector>,
}

#[sark_gen::response(raw)]
#[header("content-type", "text/plain; charset=utf-8")]
struct PlainTextResponse {
    status: StatusCode,
    body: Owned,
    #[header("x-redis-key")]
    redis_key: LocalFrameBytes,
}

#[sark_gen::request(ordered)]
struct EmptyRequest {}

#[sark_gen::handler]
async fn visit(_request: EmptyRequest, state: &AppState<'_>) -> PlainTextResponseInner<'req> {
    let body_str = match state.redis.incr(b"sark:visits").await {
        Ok(n) => format!("Visits: {}\n", n),
        Err(e) => format!("redis error: {}\n", e),
    };
    PlainTextResponseInner {
        status: StatusCode::OK,
        body: bytes_from_str(&body_str),
        redis_key: LocalFrameBytes::from_slice(b"sark:visits"),
    }
}

#[sark_gen::handler]
async fn reset(_request: EmptyRequest, state: &AppState<'_>) -> PlainTextResponseInner<'req> {
    let _ = state.redis.del(&[b"sark:visits"]).await;
    PlainTextResponseInner {
        status: StatusCode::OK,
        body: bytes_from_str("reset\n"),
        redis_key: LocalFrameBytes::from_slice(b"sark:visits"),
    }
}

sark_gen::define_route! {
    HttpWithRedisApp: AppState<'d> => {
        GET "/" => async visit,
        GET "/reset" => async reset,
    }
}

fn bytes_from_str(s: &str) -> Owned {
    let mut buf = Owned::with_capacity(s.len());
    buf.extend_from_slice(s.as_bytes());
    buf
}

#[pin_project::pin_project]
#[derive(dope_gen::Dispatcher)]
struct Dispatcher<'d, P>
where
    P: Application<Conn = sark::dispatch::conn_state::ConnState, Wire = dope::wire::Identity>
        + DateHost
        + TimerHost<'d>,
{
    #[pin]
    #[manifold(optional)]
    http: Option<Listener<1, P, Env>>,
    #[pin]
    #[manifold]
    date: Updater<2>,
    #[pin]
    #[manifold]
    redis: RedisConnector,
    #[pin]
    #[manifold]
    timer: dope::manifold::timer::Timer<{ sark::timer::SARK_TIMER_ID }>,
    _ph: std::marker::PhantomData<&'d ()>,
}

fn run_thread(
    redis_addr: SocketAddr,
    cfg: ServerCfg,
    ctx: Ctx,
    shutdown: Option<&Trigger>,
) -> io::Result<()> {
    let driver_cfg =
        <dope::DriverCfg as dope::DriverConfig>::for_tcp_profile::<Throughput>(cfg.max_conn)
            .with_cpu_id(Some(ctx.cpu));
    let mut exec = Executor::new(driver_cfg)?;

    let redis_conn = {
        let drv = exec.driver_mut();
        if let Some(trigger) = shutdown {
            trigger.register(drv);
        }
        Connector::new(
            Session::new(),
            Static::<Tcp>::new(vec![redis_addr], DEFAULT_BACKOFF),
            1,
            drv,
        )
    };
    let mut app = core::pin::pin!(Dispatcher::<_> {
        http: None::<Listener<1, _, Env>>,
        date: Updater::<2>::new(),
        redis: redis_conn,
        timer: dope::manifold::timer::Timer::new(),
        _ph: std::marker::PhantomData,
    });
    let client = app.as_mut().redis_handle();
    let timer_handle = app.as_mut().timer_handle();

    let listener_cfg = config::Config::<Tcp> {
        max_conn: cfg.max_conn,
        bind: cfg.bind,
        backlog: cfg.backlog,
        stream_opts: Default::default(),
        listener_opts: Default::default(),
    };
    let state: &'static AppState = Box::leak(Box::new(AppState { redis: client }));
    let app_state = http_with_redis_app::new(state);
    let mut http = {
        let drv = exec.driver_mut();
        Listener::<1, _, Env>::open_in(app_state, listener_cfg, drv)?
    };
    {
        let handler = http.handler_mut();
        handler.bind_timer(timer_handle, cfg.head_timeout);
        let stamp = std::ptr::NonNull::from(handler.date_stamp());
        app.as_mut().project().date.get_mut().bind(stamp);
    }
    app.as_mut().project().http.set(Some(http));
    exec.run(app.as_mut())
}

fn main() -> io::Result<()> {
    let bind: SocketAddr = std::env::var("BIND")
        .unwrap_or_else(|_| "127.0.0.1:8080".to_string())
        .parse()
        .expect("invalid BIND");
    let redis_addr: SocketAddr = std::env::var("REDIS_ADDR")
        .unwrap_or_else(|_| "127.0.0.1:6379".to_string())
        .parse()
        .expect("invalid REDIS_ADDR");

    let cfg = ServerCfg {
        bind,
        max_conn: 1024,
        backlog: 1024,
        head_timeout: std::time::Duration::from_secs(10),
    };

    eprintln!("sark http_with_redis: listening on http://{bind}, upstream redis {redis_addr}");

    Launcher::new(vec![0u16]).run(move |ctx| run_thread(redis_addr, cfg.clone(), ctx, None))
}
