use std::{collections::HashMap, env, sync::Arc};

use serde_json::Value;
use serde_json::json;
use tokio::sync::Mutex;
use wreq::{Client, Proxy};
use wreq_util::Emulation;

#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct FlareSolverrRequest {
    pub cmd: String,
    pub url: String,
    pub maxTimeout: u32,
}

#[derive(serde::Serialize, serde::Deserialize, Debug)]
pub struct FlaresolverrCookie {
    pub name: String,
    pub value: String,
    pub domain: String,
    pub path: String,
    #[serde(default)]
    pub expires: f64,
    #[serde(default)]
    pub size: u64,
    #[serde(default)]
    pub httpOnly: bool,
    #[serde(default)]
    pub secure: bool,
    #[serde(default)]
    pub session: bool,
    pub sameSite: Option<String>,
    #[serde(default)]
    pub priority: String,
    #[serde(default)]
    pub sameParty: bool,
    #[serde(default)]
    pub sourceScheme: String,
    #[serde(default)]
    pub sourcePort: u32,
    pub partitionKey: Option<String>,
}

#[derive(serde::Serialize, serde::Deserialize, Debug)]
pub struct FlareSolverrSolution {
    pub url: String,
    pub status: u32,
    pub response: String,
    pub headers: HashMap<String, String>,
    pub cookies: Vec<FlaresolverrCookie>,
    pub userAgent: String,
}

#[derive(serde::Serialize, serde::Deserialize, Debug)]
pub struct FlareSolverrResponse {
    pub status: String,
    pub message: String,
    pub solution: FlareSolverrSolution,
    pub startTimestamp: u64,
    pub endTimestamp: u64,
    pub version: String,
}

#[derive(Clone)]
pub struct Flaresolverr {
    url: String,
    proxy: bool,
}

#[derive(Debug, Default)]
struct SessionState {
    ready_session: Option<String>,
}

fn global_session_state() -> &'static Arc<Mutex<SessionState>> {
    static STATE: std::sync::OnceLock<Arc<Mutex<SessionState>>> = std::sync::OnceLock::new();
    STATE.get_or_init(|| Arc::new(Mutex::new(SessionState::default())))
}

impl Flaresolverr {
    pub fn new(url: String) -> Self {
        Self { url, proxy: false }
    }

    pub fn set_proxy(&mut self, proxy: bool) {
        self.proxy = proxy;
    }

    async fn post_payload(
        &self,
        payload: Value,
    ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
        let client = Client::builder().emulation(Emulation::Firefox136).build()?;

        let mut req = client
            .post(&self.url)
            .header("Content-Type", "application/json")
            .json(&payload);

        if self.proxy {
            if let Ok(proxy_url) = env::var("BURP_URL") {
                match Proxy::all(&proxy_url) {
                    Ok(proxy) => {
                        req = req.proxy(proxy);
                    }
                    Err(e) => {
                        eprintln!("Invalid proxy URL '{}': {}", proxy_url, e);
                    }
                }
            }
        }

        let response = req.send().await?;
        let body = response.json::<Value>().await?;
        if body
            .get("status")
            .and_then(Value::as_str)
            .is_some_and(|status| status.eq_ignore_ascii_case("error"))
        {
            let message = body
                .get("message")
                .and_then(Value::as_str)
                .unwrap_or("FlareSolverr returned status=error");
            return Err(message.to_string().into());
        }
        Ok(body)
    }

    async fn create_session(&self) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
        let body = self
            .post_payload(json!({ "cmd": "sessions.create" }))
            .await?;
        let session = body
            .get("session")
            .and_then(Value::as_str)
            .ok_or("sessions.create response missing `session`")?;
        Ok(session.to_string())
    }

    async fn destroy_session(
        &self,
        session: &str,
    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
        let _ = self
            .post_payload(json!({
                "cmd": "sessions.destroy",
                "session": session,
            }))
            .await?;
        Ok(())
    }

    async fn solve_with_session(
        &self,
        request: FlareSolverrRequest,
        session: &str,
    ) -> Result<FlareSolverrResponse, Box<dyn std::error::Error + Send + Sync>> {
        let body = self
            .post_payload(json!({
                "cmd": request.cmd,
                "url": request.url,
                "maxTimeout": request.maxTimeout,
                "session": session,
            }))
            .await?;
        let typed = serde_json::from_value::<FlareSolverrResponse>(body)?;
        Ok(typed)
    }

    async fn ensure_ready_session_locked(
        &self,
        state: &mut SessionState,
    ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
        if let Some(existing) = state.ready_session.clone() {
            return Ok(existing);
        }
        let created = self.create_session().await?;
        state.ready_session = Some(created.clone());
        Ok(created)
    }

    pub async fn solve(
        &self,
        request: FlareSolverrRequest,
    ) -> Result<FlareSolverrResponse, Box<dyn std::error::Error + Send + Sync>> {
        // Keep one ready session globally and rotate it per solve:
        // - solve with current ready session
        // - create replacement session in parallel
        // - destroy old session
        // - keep replacement as new ready session
        let session_state = global_session_state().clone();
        let mut state = session_state.lock().await;
        let active_session = self.ensure_ready_session_locked(&mut state).await?;

        let replacement_creator = {
            let solver = self.clone();
            tokio::spawn(async move { solver.create_session().await })
        };

        let solve_result = self.solve_with_session(request, &active_session).await;

        let replacement_session = match replacement_creator.await {
            Ok(Ok(session)) => session,
            Ok(Err(error)) => {
                eprintln!(
                    "FlareSolverr replacement session creation failed, retrying inline: {}",
                    error
                );
                self.create_session().await?
            }
            Err(join_error) => {
                eprintln!(
                    "FlareSolverr replacement task join failed, retrying inline: {}",
                    join_error
                );
                self.create_session().await?
            }
        };

        if let Err(error) = self.destroy_session(&active_session).await {
            eprintln!(
                "FlareSolverr session cleanup failed for session '{}': {}",
                active_session, error
            );
        }

        state.ready_session = Some(replacement_session);
        solve_result
    }
}
