use actix_web::dev::ServiceResponse;
use actix_web::http::StatusCode as ActixStatusCode;
use actix_web::web::Bytes;
use actix_web::{
    App,
    body::to_bytes,
    test::{TestRequest, call_service, init_service},
};
use anyhow::{Context, Result, anyhow, bail};
use clap::{Args, Parser, Subcommand};
use reqwest::{Client as HttpClient, Response, StatusCode};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use std::collections::HashMap;
use std::fs::{self, File};
use std::io::Write;
use std::path::{Path, PathBuf};

use crate::api::pipelines::run_pipeline;
use crate::bootstrap::Bootstrap;
use crate::config::DEFAULT_CONFIG_FILE_NAME;

const DEFAULT_CLIENTS_PATH: &str = "config/clients.json";
const DEFAULT_FETCH_URL: &str = "https://athena-db.com/gateway/fetch";

/// CLI definition for the shared Athena binary.
#[derive(Parser)]
#[command(name = "athena_rs")]
#[command(author, version, about = "Athena API server + CLI helpers")]
pub struct AthenaCli {
    /// Override the default configuration file path.
    #[arg(
        long = "config",
        alias = "config-path",
        global = true,
        value_name = "PATH",
        help = "Override the default configuration file path (default: config.yaml)"
    )]
    pub config_path: Option<PathBuf>,

    /// Override the pipeline definitions file path.
    #[arg(
        long,
        global = true,
        value_name = "PATH",
        default_value = "config/pipelines.yaml"
    )]
    pub pipelines_path: PathBuf,

    /// Override the port used when booting the API server.
    #[arg(long, global = true, value_name = "PORT")]
    pub port: Option<u16>,

    /// Only start the API server when explicitly requested.
    #[arg(long, global = true)]
    pub api_only: bool,

    /// Subcommand to run (use `--api-only` or `server` to start the API).
    #[command(subcommand)]
    pub command: Option<Command>,
}

/// Supported subcommands for Athena.
#[derive(Subcommand)]
pub enum Command {
    /// Start the Actix-based API server.
    Server,

    /// Run a pipeline definition inline.
    Pipeline(PipelineArgs),

    /// Manage saved Postgres client entries.
    Clients {
        #[command(subcommand)]
        command: ClientCommand,
    },

    /// Proxy a gateway fetch request through athena-db.com.
    Fetch(FetchArgs),

    /// Diagnostic info about the current environment.
    Diag,

    /// Print the build version and exit.
    Version,
}

/// Arguments for the pipeline runner CLI.
#[derive(Args)]
pub struct PipelineArgs {
    /// The `X-Athena-Client` header value to route the pipeline.
    #[arg(short, long, value_name = "CLIENT")]
    pub client: String,

    /// Inline JSON body describing the pipeline request.
    #[arg(long, conflicts_with = "payload_file")]
    pub payload_json: Option<String>,

    /// Path to a file containing the JSON pipeline request.
    #[arg(long, value_name = "PATH", conflicts_with = "payload_json")]
    pub payload_file: Option<PathBuf>,

    /// Reference a prebuilt pipeline by name (merged with inline overrides).
    #[arg(long, value_name = "NAME")]
    pub pipeline: Option<String>,
}

/// Subcommands for client management.
#[derive(Subcommand)]
pub enum ClientCommand {
    /// List known clients and the configured default.
    List {
        #[arg(long, value_name = "PATH", default_value = DEFAULT_CLIENTS_PATH)]
        path: PathBuf,
    },

    /// Add or update a client entry.
    Add {
        #[arg(long, value_name = "PATH", default_value = DEFAULT_CLIENTS_PATH)]
        path: PathBuf,

        /// Logical client name.
        name: String,

        /// JDBC-style URI or supabase slug.
        uri: String,
    },

    /// Remove an existing client entry.
    Remove {
        #[arg(long, value_name = "PATH", default_value = DEFAULT_CLIENTS_PATH)]
        path: PathBuf,

        /// Logical client name to delete.
        name: String,
    },

    /// Update the default client used when none is provided.
    SetDefault {
        #[arg(long, value_name = "PATH", default_value = DEFAULT_CLIENTS_PATH)]
        path: PathBuf,

        /// Name of the client to make default.
        name: String,
    },
}

/// Arguments for the gateway fetch helper command.
#[derive(Args)]
pub struct FetchArgs {
    /// Override the clients file path.
    #[arg(long, value_name = "PATH", default_value = DEFAULT_CLIENTS_PATH)]
    pub clients_path: PathBuf,

    /// Override the default client (otherwise uses the stored default).
    #[arg(long)]
    pub client: Option<String>,

    /// Custom gateway fetch URL.
    #[arg(long, value_name = "URL", default_value = DEFAULT_FETCH_URL)]
    pub url: String,

    /// Inline JSON body to POST.
    #[arg(long, conflicts_with = "body_file")]
    pub body_json: Option<String>,

    /// File containing the JSON body.
    #[arg(long, value_name = "PATH", conflicts_with = "body_json")]
    pub body_file: Option<PathBuf>,
}

/// Represents the persisted clients configuration.
#[derive(Debug, Deserialize, Serialize, Default)]
struct ClientStore {
    default: Option<String>,
    clients: HashMap<String, String>,
}

impl ClientStore {
    fn load(path: &Path) -> Result<Self> {
        if path.exists() {
            let bytes = fs::read_to_string(path).context("reading clients file")?;
            let store: ClientStore =
                serde_json::from_str(&bytes).context("parsing clients file as JSON")?;
            Ok(store)
        } else {
            Ok(Self::default())
        }
    }

    fn persist(&self, path: &Path) -> Result<()> {
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent).context("creating clients directory")?;
        }
        let mut file: File = File::create(path).context("creating clients file")?;
        serde_json::to_writer_pretty(&mut file, &self).context("serializing clients file")?;
        file.write_all(b"\n")
            .context("writing newline to clients file")?;
        Ok(())
    }
}

/// Run the pipeline CLI extension.
pub async fn run_pipeline_command(bootstrap: &Bootstrap, args: PipelineArgs) -> Result<()> {
    let mut body = match parse_json_input(args.payload_json, args.payload_file)? {
        Some(value) => value,
        None => json!({}),
    };

    if !body.is_object() {
        bail!("pipeline payload must be a JSON object");
    }

    if let Some(name) = args.pipeline.as_ref() {
        body["pipeline"] = json!(name);
    }

    if body.as_object().map_or(true, |map| map.is_empty()) && args.pipeline.is_none() {
        bail!("provide either --payload-json/--payload-file or --pipeline");
    }

    let mut service = init_service(
        App::new()
            .app_data(bootstrap.app_state.clone())
            .service(run_pipeline),
    )
    .await;

    let request = TestRequest::post()
        .uri("/pipelines")
        .insert_header(("X-Athena-Client", args.client.as_str()))
        .set_json(&body)
        .to_request();

    let response: ServiceResponse = call_service(&mut service, request).await;
    let status: ActixStatusCode = response.status();
    let body_bytes: Bytes = to_bytes(response.into_body())
        .await
        .map_err(|err| anyhow!("reading pipeline response body: {}", err))?;
    let body_text = String::from_utf8_lossy(&body_bytes);

    if status.is_success() {
        if let Ok(parsed) = serde_json::from_slice::<Value>(&body_bytes) {
            println!("{}", serde_json::to_string_pretty(&parsed)?);
        } else {
            println!("{}", body_text);
        }
    } else {
        eprintln!("pipeline request failed ({})", status);
        eprintln!("{}", body_text);
    }

    Ok(())
}

/// Run client management CLI commands.
pub fn run_clients_command(command: ClientCommand) -> Result<()> {
    match command {
        ClientCommand::List { path } => {
            let store: ClientStore = ClientStore::load(&path)?;
            println!("clients file: {}", path.display());
            println!(
                "default client: {}",
                store.default.as_deref().unwrap_or("none")
            );
            let mut names: Vec<&String> = store.clients.keys().collect();
            names.sort();
            for name in names {
                if let Some(uri) = store.clients.get(name) {
                    println!("  {} -> {}", name, uri);
                }
            }
            Ok(())
        }
        ClientCommand::Add { path, name, uri } => {
            let mut store: ClientStore = ClientStore::load(&path)?;
            store.clients.insert(name.clone(), uri.clone());
            if store.default.is_none() {
                store.default = Some(name.clone());
            }
            store.persist(&path)?;
            println!("saved client '{}' -> {} in {}", name, uri, path.display());
            Ok(())
        }
        ClientCommand::Remove { path, name } => {
            let mut store: ClientStore = ClientStore::load(&path)?;
            if store.clients.remove(&name).is_none() {
                bail!("client '{}' not found", name);
            }
            if store.default.as_deref() == Some(name.as_str()) {
                store.default = None;
            }
            store.persist(&path)?;
            println!("removed client '{}' from {}", name, path.display());
            Ok(())
        }
        ClientCommand::SetDefault { path, name } => {
            let mut store: ClientStore = ClientStore::load(&path)?;
            if !store.clients.contains_key(&name) {
                bail!("client '{}' is not defined", name);
            }
            store.default = Some(name.clone());
            store.persist(&path)?;
            println!("default client set to '{}' in {}", name, path.display());
            Ok(())
        }
    }
}

/// Run the fetch helper command.
pub async fn run_fetch_command(args: FetchArgs) -> Result<()> {
    let body: Value =
        parse_json_input(args.body_json.clone(), args.body_file.clone())?.context("empty body")?;
    let store: ClientStore = ClientStore::load(&args.clients_path)?;
    let client_name: String = if let Some(name) = args.client {
        if !store.clients.contains_key(&name) {
            bail!(
                "client '{}' not found in {}",
                name,
                args.clients_path.display()
            );
        }
        name
    } else {
        store
            .default
            .clone()
            .ok_or_else(|| anyhow!("no client provided and no default set"))?
    };

    let http: HttpClient = HttpClient::new();
    let resp: Response = http
        .post(&args.url)
        .header("Content-Type", "application/json")
        .header("X-Athena-Client", client_name.clone())
        .json(&body)
        .send()
        .await
        .context("failed to execute fetch request")?;

    let status: StatusCode = resp.status();
    let text: String = resp.text().await.context("reading fetch response")?;
    if status.is_success() {
        if let Ok(parsed) = serde_json::from_str::<Value>(&text) {
            println!("{}", serde_json::to_string_pretty(&parsed)?);
        } else {
            println!("{}", text);
        }
    } else {
        eprintln!("fetch failed ({})", status);
        eprintln!("{}", text);
    }

    Ok(())
}

/// Run a quick diagnostic summary.
pub fn run_diag_command() -> Result<()> {
    let info: Value = json!({
        "os": std::env::consts::OS,
        "arch": std::env::consts::ARCH,
        "family": std::env::consts::FAMILY,
        "cpu_count": num_cpus::get(),
        "rust_version": env!("CARGO_PKG_VERSION"),
        "hostname": std::env::var("HOSTNAME").unwrap_or_default(),
        "locale": std::env::var("LANG").unwrap_or_default(),
    });
    println!("{}", serde_json::to_string_pretty(&info)?);
    Ok(())
}

/// Print the version and exit.
pub fn run_version_command() {
    println!("{}", env!("CARGO_PKG_VERSION"));
}

fn parse_json_input(json: Option<String>, file: Option<PathBuf>) -> Result<Option<Value>> {
    if let Some(text) = json {
        let value: Value = serde_json::from_str(&text).context("parsing inline JSON payload")?;
        return Ok(Some(value));
    }
    if let Some(path) = file {
        let text: String = fs::read_to_string(&path).context("reading payload file")?;
        let value: Value = serde_json::from_str(&text).context("parsing payload file")?;
        return Ok(Some(value));
    }
    Ok(None)
}
