use anyhow::{anyhow, Context, Result};
use axum::body::Body;
use axum::extract::{Path, Query, State};
use axum::http::{header, HeaderMap, HeaderValue, Request, StatusCode, Uri};
use axum::middleware::{self, Next};
use axum::response::sse::{Event, KeepAlive, Sse};
use axum::response::{Html, IntoResponse, Response};
use axum::routing::{get, post};
use axum::{Json, Router};
use clap::Parser;
use futures_core::Stream;
use reqwest::Method;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::convert::Infallible;
use std::ffi::c_char;
use std::net::{SocketAddr, UdpSocket};
use std::sync::Arc;
use std::time::Duration;
use tokio::net::TcpListener;
use uuid::Uuid;

pub const PLUGIN_ID: &str = "aev-bridge";
pub const PLUGIN_NAME: &str = "aev-bridge";
pub const CRATE_NAME: &str = "aev-bridge";
pub const ENTRYPOINT: &str = "aev_plugin_init";
pub const PROTOCOL: &str = "aev.plugin.v1";
pub const DEFAULT_BIND: &str = "0.0.0.0:8780";
pub const DEFAULT_UPSTREAM: &str = "http://127.0.0.1:8765";
pub const PLUGIN_INIT_JSON: &str = r#"{"id":"aev-bridge","name":"aev-bridge","package":"aev-bridge","entrypoint":"aev_plugin_init","protocol":"aev.plugin.v1","capabilities":["tool","bridge","mobile","http","sse","bearer-auth"]}"#;
const PLUGIN_INIT_JSON_NUL: &str = "{\"id\":\"aev-bridge\",\"name\":\"aev-bridge\",\"package\":\"aev-bridge\",\"entrypoint\":\"aev_plugin_init\",\"protocol\":\"aev.plugin.v1\",\"capabilities\":[\"tool\",\"bridge\",\"mobile\",\"http\",\"sse\",\"bearer-auth\"]}\0";

#[no_mangle]
pub extern "C" fn aev_plugin_init() -> *const c_char {
    PLUGIN_INIT_JSON_NUL.as_ptr().cast()
}

#[derive(Parser, Debug, Clone)]
#[command(name = "aev-bridge", about = "Start the Aev phone bridge for aev-web.")]
pub struct Cli {
    #[arg(long, env = "AEV_BRIDGE_BIND", default_value = DEFAULT_BIND)]
    pub bind: SocketAddr,
    #[arg(long, env = "AEV_BRIDGE_TOKEN")]
    pub token: Option<String>,
    #[arg(long, env = "AEV_BRIDGE_UPSTREAM", default_value = DEFAULT_UPSTREAM)]
    pub upstream: String,
    #[arg(long, env = "AEV_BRIDGE_UPSTREAM_TOKEN")]
    pub upstream_token: Option<String>,
    #[arg(long, env = "AEV_BRIDGE_PUBLIC_URL")]
    pub public_url: Option<String>,
    #[arg(long, env = "AEV_BRIDGE_POLL_MS", default_value_t = 300)]
    pub poll_ms: u64,
}

#[derive(Debug, Clone)]
pub struct ServerConfig {
    pub bind: SocketAddr,
    pub token: String,
    pub upstream: String,
    pub upstream_token: String,
    pub public_url: Option<String>,
    pub poll_ms: u64,
}

impl From<Cli> for ServerConfig {
    fn from(cli: Cli) -> Self {
        let upstream_token = cli
            .upstream_token
            .or_else(|| std::env::var("AEV_WEB_TOKEN").ok())
            .unwrap_or_default();
        Self {
            bind: cli.bind,
            token: normalize_token(cli.token),
            upstream: normalize_upstream(&cli.upstream),
            upstream_token: upstream_token.trim().to_string(),
            public_url: cli
                .public_url
                .map(|url| url.trim().trim_end_matches('/').to_string()),
            poll_ms: cli.poll_ms.clamp(100, 5_000),
        }
    }
}

#[derive(Debug, Clone, Serialize)]
pub struct PluginManifest {
    pub id: &'static str,
    pub name: &'static str,
    pub package: &'static str,
    pub entrypoint: &'static str,
    pub protocol: &'static str,
    pub capabilities: Vec<&'static str>,
    pub default_bind: &'static str,
    pub default_upstream: &'static str,
}

pub fn plugin_manifest() -> PluginManifest {
    PluginManifest {
        id: PLUGIN_ID,
        name: PLUGIN_NAME,
        package: CRATE_NAME,
        entrypoint: ENTRYPOINT,
        protocol: PROTOCOL,
        capabilities: vec!["tool", "bridge", "mobile", "http", "sse", "bearer-auth"],
        default_bind: DEFAULT_BIND,
        default_upstream: DEFAULT_UPSTREAM,
    }
}

pub async fn serve(config: ServerConfig) -> Result<()> {
    if config.upstream_token.trim().is_empty() {
        return Err(anyhow!(
            "missing upstream aev-web token; set AEV_WEB_TOKEN or --upstream-token"
        ));
    }

    let bridge_url = phone_url(&config);
    let state = AppState::new(config)?;
    let app = router(state.clone());
    let listener = TcpListener::bind(state.bind)
        .await
        .with_context(|| format!("failed to bind aev-bridge to {}", state.bind))?;
    let local_addr = listener
        .local_addr()
        .context("failed to read aev-bridge listener address")?;

    eprintln!("aev-bridge listening on http://{local_addr}");
    eprintln!("phone URL: {bridge_url}/?token={}", state.token);
    eprintln!("upstream: {}", state.upstream);

    axum::serve(listener, app)
        .with_graceful_shutdown(shutdown_signal())
        .await
        .context("aev-bridge server failed")
}

pub fn router(state: AppState) -> Router {
    let auth_token = state.token.clone();
    let api = Router::new()
        .route("/manifest", get(manifest_handler))
        .route("/upstream/health", get(upstream_health_handler))
        .route(
            "/sessions",
            get(list_sessions_handler).post(create_session_handler),
        )
        .route(
            "/sessions/{id}",
            get(get_session_handler).delete(delete_session_handler),
        )
        .route("/sessions/{id}/events", get(output_events_handler))
        .route("/sessions/{id}/input", post(write_input_handler))
        .route_layer(middleware::from_fn_with_state(auth_token, require_token));

    Router::new()
        .route("/", get(index_handler))
        .route("/health", get(health_handler))
        .nest("/api", api)
        .with_state(state)
}

#[derive(Clone)]
pub struct AppState {
    bind: SocketAddr,
    token: Arc<str>,
    upstream: Arc<str>,
    upstream_token: Arc<str>,
    poll: Duration,
    client: reqwest::Client,
}

impl AppState {
    pub fn new(config: ServerConfig) -> Result<Self> {
        Ok(Self {
            bind: config.bind,
            token: Arc::<str>::from(config.token),
            upstream: Arc::<str>::from(normalize_upstream(&config.upstream)),
            upstream_token: Arc::<str>::from(config.upstream_token),
            poll: Duration::from_millis(config.poll_ms.clamp(100, 5_000)),
            client: reqwest::Client::builder()
                .build()
                .context("failed to build bridge HTTP client")?,
        })
    }

    fn upstream_url(&self, path: &str) -> String {
        format!("{}{}", self.upstream, path)
    }

    fn upstream_auth(&self, request: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
        request.bearer_auth(self.upstream_token.as_ref())
    }
}

#[derive(Debug, Deserialize)]
pub struct EventQuery {
    pub since: Option<u64>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct UpstreamOutput {
    pub id: Uuid,
    pub cursor: u64,
    pub next: u64,
    pub data: String,
    pub dropped: bool,
    pub alive: bool,
}

#[derive(Debug, Serialize)]
struct HealthResponse {
    id: &'static str,
    status: &'static str,
    protocol: &'static str,
    upstream: String,
}

#[derive(Debug, Serialize)]
struct ErrorResponse {
    error: String,
}

#[derive(Debug)]
struct ApiError {
    status: StatusCode,
    message: String,
}

impl ApiError {
    fn bad_gateway(message: impl Into<String>) -> Self {
        Self {
            status: StatusCode::BAD_GATEWAY,
            message: message.into(),
        }
    }

    fn upstream(error: reqwest::Error) -> Self {
        Self::bad_gateway(error.to_string())
    }
}

impl IntoResponse for ApiError {
    fn into_response(self) -> Response {
        (
            self.status,
            Json(ErrorResponse {
                error: self.message,
            }),
        )
            .into_response()
    }
}

async fn index_handler() -> Html<&'static str> {
    Html(INDEX_HTML)
}

async fn health_handler(State(state): State<AppState>) -> Json<HealthResponse> {
    Json(HealthResponse {
        id: PLUGIN_ID,
        status: "ok",
        protocol: PROTOCOL,
        upstream: state.upstream.to_string(),
    })
}

async fn manifest_handler() -> Json<PluginManifest> {
    Json(plugin_manifest())
}

async fn upstream_health_handler(State(state): State<AppState>) -> Result<Response, ApiError> {
    proxy_to_upstream(&state, Method::GET, "/health", None).await
}

async fn list_sessions_handler(State(state): State<AppState>) -> Result<Response, ApiError> {
    proxy_to_upstream(&state, Method::GET, "/api/sessions", None).await
}

async fn create_session_handler(
    State(state): State<AppState>,
    Json(body): Json<Value>,
) -> Result<Response, ApiError> {
    proxy_to_upstream(&state, Method::POST, "/api/sessions", Some(body)).await
}

async fn get_session_handler(
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
) -> Result<Response, ApiError> {
    proxy_to_upstream(&state, Method::GET, &format!("/api/sessions/{id}"), None).await
}

async fn write_input_handler(
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
    Json(body): Json<Value>,
) -> Result<Response, ApiError> {
    proxy_to_upstream(
        &state,
        Method::POST,
        &format!("/api/sessions/{id}/input"),
        Some(body),
    )
    .await
}

async fn delete_session_handler(
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
) -> Result<Response, ApiError> {
    proxy_to_upstream(&state, Method::DELETE, &format!("/api/sessions/{id}"), None).await
}

async fn output_events_handler(
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
    Query(query): Query<EventQuery>,
) -> Result<Sse<impl Stream<Item = Result<Event, Infallible>>>, ApiError> {
    let mut cursor = query.since.unwrap_or_default();
    let stream = async_stream::stream! {
        loop {
            match read_upstream_output(&state, id, cursor).await {
                Ok(output) => {
                    cursor = output.next;
                    if output.dropped || !output.data.is_empty() || !output.alive {
                        match serde_json::to_string(&output) {
                            Ok(json) => yield Ok(Event::default().event("output").data(json)),
                            Err(error) => {
                                let event = Event::default()
                                    .event("error")
                                    .data(error.to_string());
                                yield Ok(event);
                                break;
                            }
                        }
                    }
                    if !output.alive {
                        break;
                    }
                }
                Err(error) => {
                    let event = Event::default().event("error").data(error.message);
                    yield Ok(event);
                    break;
                }
            }
            tokio::time::sleep(state.poll).await;
        }
    };

    Ok(Sse::new(stream).keep_alive(KeepAlive::default()))
}

async fn proxy_to_upstream(
    state: &AppState,
    method: Method,
    path: &str,
    body: Option<Value>,
) -> Result<Response, ApiError> {
    let mut request = state.upstream_auth(state.client.request(method, state.upstream_url(path)));
    if let Some(body) = body {
        request = request.json(&body);
    }

    let response = request.send().await.map_err(ApiError::upstream)?;
    let status =
        StatusCode::from_u16(response.status().as_u16()).unwrap_or(StatusCode::BAD_GATEWAY);
    let content_type = response
        .headers()
        .get(reqwest::header::CONTENT_TYPE)
        .and_then(|value| value.to_str().ok())
        .and_then(|value| HeaderValue::from_str(value).ok());
    let text = response.text().await.map_err(ApiError::upstream)?;
    let mut response = (status, text).into_response();
    if let Some(content_type) = content_type {
        response
            .headers_mut()
            .insert(header::CONTENT_TYPE, content_type);
    }
    Ok(response)
}

async fn read_upstream_output(
    state: &AppState,
    id: Uuid,
    since: u64,
) -> Result<UpstreamOutput, ApiError> {
    let response = state
        .upstream_auth(
            state
                .client
                .get(state.upstream_url(&format!("/api/sessions/{id}/output?since={since}"))),
        )
        .send()
        .await
        .map_err(ApiError::upstream)?;
    if !response.status().is_success() {
        return Err(ApiError::bad_gateway(format!(
            "upstream output read failed: {}",
            response.status()
        )));
    }
    response
        .json::<UpstreamOutput>()
        .await
        .map_err(ApiError::upstream)
}

async fn require_token(State(token): State<Arc<str>>, req: Request<Body>, next: Next) -> Response {
    if request_token_matches(req.headers(), req.uri(), token.as_ref()) {
        return next.run(req).await;
    }
    (
        StatusCode::UNAUTHORIZED,
        Json(ErrorResponse {
            error: "missing or invalid bridge token".to_string(),
        }),
    )
        .into_response()
}

pub fn request_token_matches(headers: &HeaderMap, uri: &Uri, token: &str) -> bool {
    bearer_token_matches(headers, token) || query_token_matches(uri, token)
}

pub fn bearer_token_matches(headers: &HeaderMap, token: &str) -> bool {
    let Some(value) = headers
        .get(header::AUTHORIZATION)
        .and_then(|value| value.to_str().ok())
    else {
        return false;
    };
    let Some(candidate) = value.strip_prefix("Bearer ") else {
        return false;
    };
    constant_time_eq(candidate.as_bytes(), token.as_bytes())
}

fn query_token_matches(uri: &Uri, token: &str) -> bool {
    uri.query()
        .and_then(|query| {
            query.split('&').find_map(|part| {
                let (key, value) = part.split_once('=')?;
                (key == "token").then_some(value)
            })
        })
        .map(|candidate| constant_time_eq(candidate.as_bytes(), token.as_bytes()))
        .unwrap_or(false)
}

fn normalize_token(token: Option<String>) -> String {
    token
        .map(|token| token.trim().to_string())
        .filter(|token| !token.is_empty())
        .unwrap_or_else(|| Uuid::now_v7().to_string())
}

pub fn normalize_upstream(upstream: &str) -> String {
    upstream.trim().trim_end_matches('/').to_string()
}

fn phone_url(config: &ServerConfig) -> String {
    if let Some(public_url) = &config.public_url {
        return public_url.clone();
    }
    if config.bind.ip().is_unspecified() {
        if let Some(url) = lan_url(config.bind.port()) {
            return url;
        }
    }
    format!("http://{}", config.bind)
}

fn lan_url(port: u16) -> Option<String> {
    let socket = UdpSocket::bind("0.0.0.0:0").ok()?;
    socket.connect("8.8.8.8:80").ok()?;
    let local_addr = socket.local_addr().ok()?;
    Some(format!("http://{}:{port}", local_addr.ip()))
}

fn constant_time_eq(left: &[u8], right: &[u8]) -> bool {
    if left.len() != right.len() {
        return false;
    }
    left.iter()
        .zip(right)
        .fold(0_u8, |diff, (left, right)| diff | (left ^ right))
        == 0
}

async fn shutdown_signal() {
    let _ = tokio::signal::ctrl_c().await;
}

const INDEX_HTML: &str = r#"<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>aev-bridge</title>
  <style>
    :root { color-scheme: dark; font-family: Inter, ui-sans-serif, system-ui, sans-serif; background: #07090f; color: #f5f7fb; }
    body { margin: 0; min-height: 100vh; background: linear-gradient(135deg, #07111f, #140b24 55%, #05070b); }
    main { min-height: 100vh; display: grid; grid-template-rows: auto auto 1fr auto; }
    header { padding: 22px 18px 14px; display: flex; justify-content: space-between; gap: 12px; align-items: end; }
    h1 { margin: 0; font-size: clamp(28px, 10vw, 58px); letter-spacing: -0.06em; }
    .tag { color: #9da7bd; font-size: 13px; }
    .bar { display: grid; grid-template-columns: 1fr auto; gap: 9px; padding: 0 14px 12px; }
    input, textarea, button { border: 1px solid #2f3a50; border-radius: 14px; background: #0e1420; color: #f5f7fb; font: inherit; }
    input, textarea { padding: 13px 14px; min-width: 0; }
    button { padding: 13px 15px; background: #6d5dfc; border-color: #877cff; font-weight: 800; }
    button.secondary { background: #182033; border-color: #2f3a50; }
    pre { margin: 0 14px; border: 1px solid #253047; border-radius: 18px; background: #02040a; padding: 14px; overflow: auto; white-space: pre-wrap; word-break: break-word; font: 13px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: #cbffd8; }
    footer { padding: 12px 14px 16px; display: grid; grid-template-columns: 1fr auto auto; gap: 9px; }
    textarea { min-height: 46px; resize: vertical; }
    @media (max-width: 620px) { footer, .bar { grid-template-columns: 1fr; } header { display: block; } }
  </style>
</head>
<body>
  <main>
    <header>
      <div>
        <h1>aev-bridge</h1>
        <div class="tag">HTTP + SSE bridge to aev-web</div>
      </div>
      <div id="status" class="tag">offline</div>
    </header>
    <section class="bar">
      <input id="token" type="password" autocomplete="off" placeholder="Bridge token">
      <button id="start">Start Session</button>
    </section>
    <pre id="screen"></pre>
    <footer>
      <textarea id="input" placeholder="Command, then Enter"></textarea>
      <button id="send">Send</button>
      <button id="ctrlc" class="secondary">Ctrl-C</button>
    </footer>
  </main>
  <script>
    const statusEl = document.querySelector('#status');
    const screenEl = document.querySelector('#screen');
    const tokenEl = document.querySelector('#token');
    const inputEl = document.querySelector('#input');
    let sessionId = null;
    let events = null;

    tokenEl.value = new URLSearchParams(location.search).get('token') || localStorage.getItem('aevBridgeToken') || '';

    function token() {
      const value = tokenEl.value.trim();
      if (value) localStorage.setItem('aevBridgeToken', value);
      return value;
    }

    async function api(path, options = {}) {
      const headers = Object.assign({ 'Authorization': `Bearer ${token()}` }, options.headers || {});
      if (options.body) headers['Content-Type'] = 'application/json';
      const response = await fetch(`/api${path}`, Object.assign({}, options, { headers }));
      const body = await response.json().catch(() => ({}));
      if (!response.ok) throw new Error(body.error || response.statusText);
      return body;
    }

    function connectEvents() {
      if (events) events.close();
      events = new EventSource(`/api/sessions/${sessionId}/events?token=${encodeURIComponent(token())}`);
      events.addEventListener('output', event => {
        const output = JSON.parse(event.data);
        if (output.dropped) screenEl.textContent += '\n[bridge: output truncated]\n';
        if (output.data) {
          screenEl.textContent += output.data;
          screenEl.scrollTop = screenEl.scrollHeight;
        }
        if (!output.alive) statusEl.textContent = `session ${sessionId} exited`;
      });
      events.addEventListener('error', () => { statusEl.textContent = 'event stream disconnected'; });
    }

    async function start() {
      if (!token()) {
        statusEl.textContent = 'token required';
        return;
      }
      const result = await api('/sessions', { method: 'POST', body: JSON.stringify({}) });
      sessionId = result.session.id;
      screenEl.textContent = '';
      statusEl.textContent = `session ${sessionId}`;
      connectEvents();
    }

    async function send(data) {
      if (!sessionId) await start();
      if (!sessionId || !data) return;
      await api(`/sessions/${sessionId}/input`, { method: 'POST', body: JSON.stringify({ data }) });
    }

    document.querySelector('#start').addEventListener('click', () => start().catch(error => statusEl.textContent = error.message));
    document.querySelector('#send').addEventListener('click', () => {
      const data = inputEl.value;
      inputEl.value = '';
      send(data).catch(error => statusEl.textContent = error.message);
    });
    document.querySelector('#ctrlc').addEventListener('click', () => send('\u0003').catch(error => statusEl.textContent = error.message));
    inputEl.addEventListener('keydown', event => {
      if (event.key === 'Enter' && !event.shiftKey) {
        event.preventDefault();
        const data = `${inputEl.value}\r`;
        inputEl.value = '';
        send(data).catch(error => statusEl.textContent = error.message);
      }
    });
  </script>
</body>
</html>"#;

#[cfg(test)]
mod tests {
    use super::*;
    use axum::http::HeaderValue;
    use std::ffi::CStr;

    #[test]
    fn manifest_matches_plugin_contract() {
        let manifest = plugin_manifest();

        assert_eq!(manifest.id, "aev-bridge");
        assert_eq!(manifest.package, "aev-bridge");
        assert_eq!(manifest.entrypoint, "aev_plugin_init");
        assert_eq!(manifest.protocol, "aev.plugin.v1");
        assert!(manifest.capabilities.contains(&"sse"));
    }

    #[test]
    fn ffi_entrypoint_returns_static_manifest_json() {
        assert!(!aev_plugin_init().is_null());
        let json = unsafe { CStr::from_ptr(aev_plugin_init()) }
            .to_str()
            .expect("plugin manifest is utf-8");
        assert_eq!(json, PLUGIN_INIT_JSON);
    }

    #[test]
    fn auth_accepts_bearer_or_query_token() {
        let mut headers = HeaderMap::new();
        let uri = Uri::from_static("/api/sessions?token=secret");
        assert!(request_token_matches(&headers, &uri, "secret"));

        headers.insert(
            header::AUTHORIZATION,
            HeaderValue::from_static("Bearer secret"),
        );
        let uri = Uri::from_static("/api/sessions");
        assert!(request_token_matches(&headers, &uri, "secret"));
        assert!(!request_token_matches(&headers, &uri, "different"));
    }

    #[test]
    fn upstream_base_url_is_normalized() {
        assert_eq!(
            normalize_upstream(" http://127.0.0.1:8765/// "),
            "http://127.0.0.1:8765"
        );
    }

    #[test]
    fn router_constructs_with_capture_syntax() {
        let config = ServerConfig {
            bind: DEFAULT_BIND.parse().expect("bind parses"),
            token: "bridge".to_string(),
            upstream: DEFAULT_UPSTREAM.to_string(),
            upstream_token: "upstream".to_string(),
            public_url: None,
            poll_ms: 300,
        };
        let state = AppState::new(config).expect("state builds");
        let _ = router(state);
    }
}
