use std::{
    net::SocketAddr,
    pin::Pin,
    sync::{Arc, RwLock},
};

use anyhow::Context;
use http_req::request::{RedirectPolicy, Request};
use rand::seq::SliceRandom as _;
use url::Url;

use crate::{
    common::{error::Error, global_ctx::ArcGlobalCtx},
    tunnel::{IpVersion, Tunnel, TunnelConnector, TunnelError, ZCPacketSink, ZCPacketStream},
};

use crate::proto::common::TunnelInfo;

use super::create_connector_by_url;

pub struct TunnelWithInfo {
    inner: Box<dyn Tunnel>,
    info: TunnelInfo,
}

impl TunnelWithInfo {
    pub fn new(inner: Box<dyn Tunnel>, info: TunnelInfo) -> Self {
        Self { inner, info }
    }
}

impl Tunnel for TunnelWithInfo {
    fn split(&self) -> (Pin<Box<dyn ZCPacketStream>>, Pin<Box<dyn ZCPacketSink>>) {
        self.inner.split()
    }

    fn info(&self) -> Option<TunnelInfo> {
        Some(self.info.clone())
    }
}

#[derive(Debug, PartialEq, Copy, Clone)]
enum HttpRedirectType {
    Unknown,
    // redirected url is in the path of new url
    RedirectToQuery,
    // redirected url is the entire new url
    RedirectToUrl,
    // redirected url is in the body of response
    BodyUrls,
}

#[derive(Debug)]
pub struct HttpTunnelConnector {
    addr: url::Url,
    bind_addrs: Vec<SocketAddr>,
    ip_version: IpVersion,
    global_ctx: ArcGlobalCtx,
    redirect_type: HttpRedirectType,
}

impl HttpTunnelConnector {
    pub fn new(addr: url::Url, global_ctx: ArcGlobalCtx) -> Self {
        Self {
            addr,
            bind_addrs: Vec::new(),
            ip_version: IpVersion::Both,
            global_ctx,
            redirect_type: HttpRedirectType::Unknown,
        }
    }

    #[tracing::instrument(ret)]
    async fn handle_302_redirect(
        &mut self,
        new_url: url::Url,
        url_str: &str,
    ) -> Result<Box<dyn TunnelConnector>, Error> {
        // the url should be in following format:
        // 1: http(s)://easytier.cn/?url=tcp://10.147.22.22:11010 (scheme is http, domain is ignored, path is splitted into proto type and addr)
        // 2: http(s)://tcp://10.137.22.22:11010 (connector url is appended to the scheme)
        // 3: tcp://10.137.22.22:11010 (scheme is protocol type, the url is used to construct a connector directly)
        tracing::info!("redirect to {}", new_url);
        let url = url::Url::parse(new_url.as_str())
            .with_context(|| format!("parsing redirect url failed. url: {}", new_url))?;
        if url.scheme() == "http" || url.scheme() == "https" {
            let mut query = new_url
                .query_pairs()
                .filter_map(|x| url::Url::parse(&x.1).ok())
                .collect::<Vec<_>>();
            query.shuffle(&mut rand::thread_rng());
            if !query.is_empty() {
                tracing::info!("try to create connector by url: {}", query[0]);
                self.redirect_type = HttpRedirectType::RedirectToQuery;
                return create_connector_by_url(
                    query[0].as_ref(),
                    &self.global_ctx,
                    self.ip_version,
                )
                .await;
            } else if let Some(new_url) = url_str
                .strip_prefix(format!("{}://", url.scheme()).as_str())
                .and_then(|x| Url::parse(x).ok())
            {
                // stripe the scheme and create connector by url
                self.redirect_type = HttpRedirectType::RedirectToUrl;
                return create_connector_by_url(
                    new_url.as_str(),
                    &self.global_ctx,
                    self.ip_version,
                )
                .await;
            }
            return Err(Error::InvalidUrl(format!(
                "no valid connector url found in url: {}",
                url
            )));
        } else {
            self.redirect_type = HttpRedirectType::RedirectToUrl;
            return create_connector_by_url(new_url.as_str(), &self.global_ctx, self.ip_version)
                .await;
        }
    }

    #[tracing::instrument]
    async fn handle_200_success(
        &mut self,
        body: &String,
    ) -> Result<Box<dyn TunnelConnector>, Error> {
        // resp body should be line of connector urls, like:
        // tcp://10.1.1.1:11010
        // udp://10.1.1.1:11010
        let mut lines = body
            .lines()
            .map(|line| line.trim())
            .filter(|line| !line.is_empty())
            .collect::<Vec<&str>>();

        tracing::info!("get {} lines of connector urls", lines.len());

        // shuffle the lines and pick the usable one
        lines.shuffle(&mut rand::thread_rng());

        for line in lines {
            let url = url::Url::parse(line);
            if url.is_err() {
                tracing::warn!("invalid url: {}, skip it", line);
                continue;
            }
            self.redirect_type = HttpRedirectType::BodyUrls;
            return create_connector_by_url(line, &self.global_ctx, self.ip_version).await;
        }

        Err(Error::InvalidUrl(format!(
            "no valid connector url found, response body: {}",
            body
        )))
    }

    #[tracing::instrument(ret)]
    pub async fn get_redirected_connector(
        &mut self,
        original_url: &str,
    ) -> Result<Box<dyn TunnelConnector>, Error> {
        self.redirect_type = HttpRedirectType::Unknown;
        tracing::info!("get_redirected_url: {}", original_url);
        // Container for body of a response.
        let body = Arc::new(RwLock::new(Vec::new()));

        let original_url_clone = original_url.to_string();
        let body_clone = body.clone();
        let res = tokio::task::spawn_blocking(move || {
            let uri = http_req::uri::Uri::try_from(original_url_clone.as_ref())
                .with_context(|| format!("parsing url failed. url: {}", original_url_clone))?;

            tracing::info!("sending http request to {}", uri);

            Request::new(&uri)
                .redirect_policy(RedirectPolicy::Limit(0))
                .timeout(std::time::Duration::from_secs(20))
                .send(&mut *body_clone.write().unwrap())
                .with_context(|| format!("sending http request failed. url: {}", uri))
        })
        .await
        .map_err(|e| Error::InvalidUrl(format!("task join error: {}", e)))??;

        let body = String::from_utf8_lossy(&body.read().unwrap()).to_string();

        if res.status_code().is_redirect() {
            let redirect_url = res
                .headers()
                .get("Location")
                .ok_or_else(|| Error::InvalidUrl("no redirect address found".to_string()))?;
            let new_url = url::Url::parse(redirect_url.as_str())
                .with_context(|| format!("parsing redirect url failed. url: {}", redirect_url))?;
            return self.handle_302_redirect(new_url, redirect_url).await;
        } else if res.status_code().is_success() {
            return self.handle_200_success(&body).await;
        } else {
            return Err(Error::InvalidUrl(format!(
                "unexpected response, resp: {:?}, body: {}",
                res, body,
            )));
        }
    }
}

#[async_trait::async_trait]
impl super::TunnelConnector for HttpTunnelConnector {
    async fn connect(&mut self) -> Result<Box<dyn Tunnel>, TunnelError> {
        let mut conn = self
            .get_redirected_connector(self.addr.to_string().as_str())
            .await
            .with_context(|| "get redirected url failed")?;
        conn.set_ip_version(self.ip_version);
        let t = conn.connect().await?;
        let info = t.info().unwrap_or_default();
        Ok(Box::new(TunnelWithInfo::new(
            t,
            TunnelInfo {
                local_addr: info.local_addr.clone(),
                remote_addr: Some(self.addr.clone().into()),
                tunnel_type: format!(
                    "{:?}-{}",
                    self.redirect_type,
                    info.remote_addr.unwrap_or_default()
                ),
            },
        )))
    }

    fn remote_url(&self) -> url::Url {
        self.addr.clone()
    }

    fn set_bind_addrs(&mut self, addrs: Vec<SocketAddr>) {
        self.bind_addrs = addrs;
    }

    fn set_ip_version(&mut self, ip_version: IpVersion) {
        self.ip_version = ip_version;
    }
}

#[cfg(test)]
mod tests {
    use tokio::{io::AsyncWriteExt as _, net::TcpListener};

    use crate::{
        common::global_ctx::tests::get_mock_global_ctx,
        tunnel::{tcp::TcpTunnelListener, TunnelConnector, TunnelListener},
    };

    use super::*;

    async fn run_http_redirect_server(port: u16, test_type: HttpRedirectType) -> Result<(), Error> {
        let listener = TcpListener::bind(format!("0.0.0.0:{}", port)).await?;
        let (mut stream, _) = listener.accept().await?;

        match test_type {
            HttpRedirectType::RedirectToQuery => {
                let resp = "HTTP/1.1 301 Moved Permanently\r\nLocation: http://test.com/?url=tcp://127.0.0.1:25888\r\n\r\n";
                stream.write_all(resp.as_bytes()).await?;
            }
            HttpRedirectType::RedirectToUrl => {
                let resp =
                    "HTTP/1.1 301 Moved Permanently\r\nLocation: tcp://127.0.0.1:25888\r\n\r\n";
                stream.write_all(resp.as_bytes()).await?;
            }
            HttpRedirectType::BodyUrls => {
                let resp = "HTTP/1.1 200 OK\r\n\r\ntcp://127.0.0.1:25888";
                stream.write_all(resp.as_bytes()).await?;
            }
            HttpRedirectType::Unknown => {
                panic!("unexpected test type");
            }
        }

        Ok(())
    }

    #[rstest::rstest]
    #[serial_test::serial(http_redirect_test)]
    #[tokio::test]
    async fn http_redirect_test(
        // 1. 301 redirect
        // 2. 200 success with valid connector urls
        #[values(
            HttpRedirectType::RedirectToQuery,
            HttpRedirectType::RedirectToUrl,
            HttpRedirectType::BodyUrls
        )]
        test_type: HttpRedirectType,
    ) {
        let http_task = tokio::spawn(run_http_redirect_server(35888, test_type));
        tokio::time::sleep(std::time::Duration::from_millis(10)).await;
        let test_url: url::Url = "http://127.0.0.1:35888".parse().unwrap();
        let global_ctx = get_mock_global_ctx();
        let mut flags = global_ctx.config.get_flags();
        flags.bind_device = false;
        global_ctx.config.set_flags(flags);
        let mut connector = HttpTunnelConnector::new(test_url.clone(), global_ctx.clone());

        let mut listener = TcpTunnelListener::new("tcp://0.0.0.0:25888".parse().unwrap());
        listener.listen().await.unwrap();

        let task = tokio::spawn(async move {
            let _conn = listener.accept().await.unwrap();
        });

        let t = connector.connect().await.unwrap();
        assert_eq!(connector.redirect_type, test_type);
        let info = t.info().unwrap();
        let remote_addr = info.remote_addr.unwrap();
        assert_eq!(remote_addr, test_url.into());

        tokio::join!(task).0.unwrap();
        tokio::join!(http_task).0.unwrap().unwrap();
    }
}
