extern crate hyper;
extern crate serde;

use std::{path::PathBuf, pin::Pin, str, task::Poll};

use crate::api::event_socket::{EventSocket, SinkMessage, SocketEvent, SocketUpdate};
use crate::api::AuthenticationStore;
use bytes::Bytes;
use hyper::{Body, Client, Method, Request, Uri};
use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder};

use async_trait::async_trait;
use serde::{de::DeserializeOwned, Deserialize, Serialize};

use tokio::net::TcpStream;

use futures_util::stream::{BoxStream, Stream};
use futures_util::task::Context;
use futures_util::{Sink, SinkExt, StreamExt, TryStreamExt};

use tokio_tungstenite::connect_async;
use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};

use crate::{
    model::{
        Conversation, ConversationID, Event, JwtToken, Message, MessageID, OutgoingMessage,
        UpdateItem,
    },
    APIInterface,
};

type HttpClient = Client<HttpsConnector<hyper::client::HttpConnector>>;

pub struct HTTPAPIClient<K: AuthenticationStore + Send + Sync> {
    pub base_url: Uri,
    pub auth_store: K,
    client: HttpClient,
}

#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct Credentials {
    pub username: String,
    pub password: String,
}

#[derive(Debug)]
pub enum Error {
    ClientError(String),
    HTTPError(hyper::Error),
    SerdeError(serde_json::Error),
    DecodeError(String),
    PongError(tungstenite::Error),
    URLError,
    Unauthorized,
}

impl std::error::Error for Error {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Error::HTTPError(ref err) => Some(err),
            _ => None,
        }
    }
}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{:?}", self)
    }
}

impl From<hyper::Error> for Error {
    fn from(err: hyper::Error) -> Error {
        Error::HTTPError(err)
    }
}

impl From<serde_json::Error> for Error {
    fn from(err: serde_json::Error) -> Error {
        Error::SerdeError(err)
    }
}

impl From<tungstenite::Error> for Error {
    fn from(err: tungstenite::Error) -> Error {
        Error::ClientError(err.to_string())
    }
}

trait AuthBuilder {
    fn with_auth_string(self, token: &Option<String>) -> Self;
}

impl AuthBuilder for hyper::http::request::Builder {
    fn with_auth_string(self, token: &Option<String>) -> Self {
        if let Some(token) = &token {
            self.header("Authorization", format!("Bearer: {}", token))
        } else {
            self
        }
    }
}

#[cfg(test)]
#[allow(dead_code)]
trait AuthSetting {
    fn authenticate(&mut self, token: &Option<JwtToken>);
}

#[cfg(test)]
impl<B> AuthSetting for hyper::http::Request<B> {
    fn authenticate(&mut self, token: &Option<JwtToken>) {
        if let Some(token) = &token {
            self.headers_mut()
                .insert("Authorization", token.to_header_value());
        }
    }
}

type WebsocketSink = futures_util::stream::SplitSink<
    WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>,
    tungstenite::Message,
>;
type WebsocketStream =
    futures_util::stream::SplitStream<WebSocketStream<MaybeTlsStream<TcpStream>>>;

pub struct WebsocketEventSocket {
    sink: Option<WebsocketSink>,
    stream: WebsocketStream,
}

impl WebsocketEventSocket {
    pub fn new(socket: WebSocketStream<MaybeTlsStream<TcpStream>>) -> Self {
        let (sink, stream) = socket.split();

        Self {
            sink: Some(sink),
            stream,
        }
    }
}

impl WebsocketEventSocket {
    fn raw_update_stream(self) -> impl Stream<Item = Result<SocketUpdate, Error>> {
        self.stream
            .map_err(Error::from)
            .try_filter_map(|msg| async move {
                match msg {
                    tungstenite::Message::Text(text) => {
                        match serde_json::from_str::<Vec<UpdateItem>>(&text) {
                            Ok(updates) => Ok(Some(SocketUpdate::Update(updates))),
                            Err(e) => {
                                log::error!("Error parsing update: {:?}", e);
                                Err(Error::from(e))
                            }
                        }
                    }
                    tungstenite::Message::Ping(_) => {
                        // We don't expect the server to send us pings.
                        Ok(None)
                    }
                    tungstenite::Message::Pong(_) => Ok(Some(SocketUpdate::Pong)),
                    tungstenite::Message::Close(_) => {
                        // Connection was closed cleanly
                        Err(Error::ClientError("WebSocket connection closed".into()))
                    }
                    _ => Ok(None),
                }
            })
    }
}

#[async_trait]
impl EventSocket for WebsocketEventSocket {
    type Error = Error;
    type EventStream = BoxStream<'static, Result<SocketEvent, Error>>;
    type UpdateStream = BoxStream<'static, Result<SocketUpdate, Error>>;

    async fn events(
        mut self,
    ) -> (
        Self::EventStream,
        impl Sink<SinkMessage, Error = Self::Error>,
    ) {
        use futures_util::stream::iter;

        let sink = self.sink.take().unwrap().with(|f| match f {
            SinkMessage::Ping => futures_util::future::ready(Ok::<tungstenite::Message, Error>(
                tungstenite::Message::Ping(Bytes::new()),
            )),
        });

        let stream = self
            .raw_update_stream()
            .map_ok(
                |updates| -> BoxStream<'static, Result<SocketEvent, Error>> {
                    match updates {
                        SocketUpdate::Update(updates) => {
                            let iter_stream = iter(
                                updates
                                    .into_iter()
                                    .map(|u| Ok(SocketEvent::Update(Event::from(u)))),
                            );
                            iter_stream.boxed()
                        }
                        SocketUpdate::Pong => iter(std::iter::once(Ok(SocketEvent::Pong))).boxed(),
                    }
                },
            )
            .try_flatten()
            .boxed();

        (stream, sink)
    }

    async fn raw_updates(self) -> Self::UpdateStream {
        self.raw_update_stream().boxed()
    }
}

pub struct ResponseStream {
    body: hyper::Body,
}

impl Stream for ResponseStream {
    type Item = Result<Bytes, Error>;

    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
        self.body.poll_next_unpin(cx).map_err(Error::HTTPError)
    }
}

impl From<hyper::Body> for ResponseStream {
    fn from(value: hyper::Body) -> Self {
        ResponseStream { body: value }
    }
}

#[async_trait]
impl<K: AuthenticationStore + Send + Sync> APIInterface for HTTPAPIClient<K> {
    type Error = Error;
    type ResponseStream = ResponseStream;

    async fn get_version(&mut self) -> Result<String, Self::Error> {
        let version: String = self.deserialized_response("version", Method::GET).await?;
        Ok(version)
    }

    async fn get_conversations(&mut self) -> Result<Vec<Conversation>, Self::Error> {
        let conversations: Vec<Conversation> = self
            .deserialized_response("conversations", Method::GET)
            .await?;
        Ok(conversations)
    }

    async fn authenticate(&mut self, credentials: Credentials) -> Result<JwtToken, Self::Error> {
        #[derive(Deserialize, Debug)]
        struct AuthResponse {
            jwt: String,
        }

        log::debug!("Authenticating with username: {:?}", credentials.username);

        let body = || -> Body { serde_json::to_string(&credentials).unwrap().into() };
        let token: AuthResponse = self
            .deserialized_response_with_body_retry("authenticate", Method::POST, body, false)
            .await?;
        let token = JwtToken::new(&token.jwt).map_err(|e| Error::DecodeError(e.to_string()))?;

        log::debug!("Saving token: {:?}", token);
        self.auth_store.set_token(token.to_string()).await;

        Ok(token)
    }

    async fn mark_conversation_as_read(
        &mut self,
        conversation_id: &ConversationID,
    ) -> Result<(), Self::Error> {
        // SERVER JANK: This should be POST, but it's GET for some reason.
        let endpoint = format!("markConversation?guid={}", conversation_id);
        self.response_with_body_retry(&endpoint, Method::GET, Body::empty, true)
            .await?;
        Ok(())
    }

    async fn get_messages(
        &mut self,
        conversation_id: &ConversationID,
        limit: Option<u32>,
        before: Option<MessageID>,
        after: Option<MessageID>,
    ) -> Result<Vec<Message>, Self::Error> {
        let mut endpoint = format!("messages?guid={}", conversation_id);

        if let Some(limit_val) = limit {
            endpoint.push_str(&format!("&limit={}", limit_val));
        }

        if let Some(before_id) = before {
            endpoint.push_str(&format!("&beforeMessageGUID={}", before_id));
        }

        if let Some(after_id) = after {
            endpoint.push_str(&format!("&afterMessageGUID={}", after_id));
        }

        let messages: Vec<Message> = self.deserialized_response(&endpoint, Method::GET).await?;
        Ok(messages)
    }

    async fn send_message(
        &mut self,
        outgoing_message: &OutgoingMessage,
    ) -> Result<Message, Self::Error> {
        let message: Message = self
            .deserialized_response_with_body("sendMessage", Method::POST, || {
                serde_json::to_string(&outgoing_message).unwrap().into()
            })
            .await?;

        Ok(message)
    }

    async fn fetch_attachment_data(
        &mut self,
        guid: &str,
        preview: bool,
    ) -> Result<ResponseStream, Self::Error> {
        let endpoint = format!("attachment?guid={}&preview={}", guid, preview);
        self.response_with_body_retry(&endpoint, Method::GET, Body::empty, true)
            .await
            .map(hyper::Response::into_body)
            .map(ResponseStream::from)
    }

    async fn upload_attachment<R>(
        &mut self,
        mut data: tokio::io::BufReader<R>,
        filename: &str,
        _size: u64,
    ) -> Result<String, Self::Error>
    where
        R: tokio::io::AsyncRead + Unpin + Send + Sync + 'static,
    {
        use tokio::io::AsyncReadExt;

        #[derive(Deserialize, Debug)]
        struct UploadAttachmentResponse {
            #[serde(rename = "fileTransferGUID")]
            guid: String,
        }

        // TODO: We can still use Body::wrap_stream here, but we need to make sure to plumb the CONTENT_LENGTH header,
        // otherwise CocoaHTTPServer will crash because of a bug.
        //
        // See ff03e73758f30c081a9319a8c04025cba69b8393 for what this was like before.
        let mut bytes = Vec::new();
        data.read_to_end(&mut bytes)
            .await
            .map_err(|e| Error::ClientError(e.to_string()))?;

        let encoded_filename = urlencoding::encode(filename);
        let endpoint = format!("uploadAttachment?filename={}", encoded_filename);
        let mut bytes_opt = Some(bytes);

        let response: UploadAttachmentResponse = self
            .deserialized_response_with_body_retry(
                &endpoint,
                Method::POST,
                move || {
                    Body::from(
                        bytes_opt
                            .take()
                            .expect("Body already consumed during retry"),
                    )
                },
                false,
            )
            .await?;

        Ok(response.guid)
    }

    async fn open_event_socket(
        &mut self,
        update_seq: Option<u64>,
    ) -> Result<WebsocketEventSocket, Self::Error> {
        use tungstenite::handshake::client::generate_key;
        use tungstenite::handshake::client::Request as TungsteniteRequest;

        let endpoint = match update_seq {
            Some(seq) => format!("updates?seq={}", seq),
            None => "updates".to_string(),
        };

        let uri = self
            .uri_for_endpoint(&endpoint, Some(self.websocket_scheme()))?;

        loop {
            log::debug!("Connecting to websocket: {:?}", uri);

            let auth = self.auth_store.get_token().await;
            let host = uri.authority().unwrap().host();
            let mut request = TungsteniteRequest::builder()
                .header("Host", host)
                .header("Connection", "Upgrade")
                .header("Upgrade", "websocket")
                .header("Sec-WebSocket-Version", "13")
                .header("Sec-WebSocket-Key", generate_key())
                .uri(uri.to_string())
                .body(())
                .expect("Unable to build websocket request");

            match &auth {
                Some(token) => {
                    request.headers_mut().insert(
                        "Authorization",
                        format!("Bearer: {}", token).parse().unwrap(),
                    );
                }
                None => {
                    log::warn!(target: "websocket", "Proceeding without auth token.");
                }
            }

            log::debug!("Websocket request: {:?}", request);

            let mut should_retry = true; // retry once after authenticating. 
            match connect_async(request).await.map_err(Error::from) {
                Ok((socket, response)) => {
                    log::debug!("Websocket connected: {:?}", response.status());
                    break Ok(WebsocketEventSocket::new(socket))
                }
                Err(e) => match &e {
                    Error::ClientError(ce) => match ce.as_str() {
                        "HTTP error: 401 Unauthorized" | "Unauthorized" => {
                            // Try to authenticate
                            if let Some(credentials) = &self.auth_store.get_credentials().await {
                                log::warn!("Websocket connection failed, attempting to authenticate");
                                let new_token = self.authenticate(credentials.clone()).await?;
                                self.auth_store.set_token(new_token.to_string()).await;

                                if should_retry {
                                    // try again on the next attempt.
                                    continue;
                                } else {
                                    break Err(e);
                                }
                            } else {
                                log::error!("Websocket unauthorized, no credentials provided");
                                break Err(Error::ClientError(
                                    "Unauthorized, no credentials provided".into(),
                                ));
                            }
                        }
                        _ => break Err(e),
                    },

                    _ => break Err(e),
                },
            }
        }
    }
}

impl<K: AuthenticationStore + Send + Sync> HTTPAPIClient<K> {
    pub fn new(base_url: Uri, auth_store: K) -> HTTPAPIClient<K> {
        let https = HttpsConnectorBuilder::new()
            .with_webpki_roots()
            .https_or_http()
            .enable_http1()
            .build();
        let client = Client::builder().build::<_, Body>(https);

        HTTPAPIClient { base_url, auth_store, client }
    }

    fn uri_for_endpoint(&self, endpoint: &str, scheme: Option<&str>) -> Result<Uri, Error> {
        let mut parts = self.base_url.clone().into_parts();
        let root_path: PathBuf = parts
            .path_and_query
            .ok_or(Error::URLError)?
            .path()
            .into();

        let path = root_path.join(endpoint);
        let path_str = path.to_str().ok_or(Error::URLError)?;
        parts.path_and_query = Some(path_str.parse().map_err(|_| Error::URLError)?);

        if let Some(scheme) = scheme {
            parts.scheme = Some(scheme.parse().map_err(|_| Error::URLError)?);
        }

        Uri::try_from(parts).map_err(|_| Error::URLError)
    }

    fn websocket_scheme(&self) -> &str {
        if self.base_url.scheme().unwrap() == "https" {
            "wss"
        } else {
            "ws"
        }
    }

    async fn deserialized_response<T: DeserializeOwned>(
        &mut self,
        endpoint: &str,
        method: Method,
    ) -> Result<T, Error> {
        self.deserialized_response_with_body(endpoint, method, Body::empty)
            .await
    }

    async fn deserialized_response_with_body<T>(
        &mut self,
        endpoint: &str,
        method: Method,
        body_fn: impl FnMut() -> Body,
    ) -> Result<T, Error>
    where
        T: DeserializeOwned,
    {
        self.deserialized_response_with_body_retry(endpoint, method, body_fn, true)
            .await
    }

    async fn deserialized_response_with_body_retry<T>(
        &mut self,
        endpoint: &str,
        method: Method,
        body_fn: impl FnMut() -> Body,
        retry_auth: bool,
    ) -> Result<T, Error>
    where
        T: DeserializeOwned,
    {
        let response = self
            .response_with_body_retry(endpoint, method, body_fn, retry_auth)
            .await?;

        // Read and parse response body
        let body = hyper::body::to_bytes(response.into_body()).await?;
        let parsed: T = match serde_json::from_slice(&body) {
            Ok(result) => Ok(result),
            Err(json_err) => {
                log::error!("Error deserializing JSON: {:?}", json_err);
                log::error!("Body: {:?}", String::from_utf8_lossy(&body));

                // If JSON deserialization fails, try to interpret it as plain text
                // Unfortunately the server does return things like this...
                let s = str::from_utf8(&body).map_err(|e| Error::DecodeError(e.to_string()))?;
                serde_plain::from_str(s).map_err(|_| json_err)
            }
        }?;

        Ok(parsed)
    }

    async fn response_with_body_retry(
        &mut self,
        endpoint: &str,
        method: Method,
        mut body_fn: impl FnMut() -> Body,
        retry_auth: bool,
    ) -> Result<hyper::Response<Body>, Error> {
        use hyper::StatusCode;

        let uri = self.uri_for_endpoint(endpoint, None)?;
        log::debug!("Requesting {:?} {:?}", method, uri);

        let mut build_request = |auth: &Option<String>| {
            let body = body_fn();
            Request::builder()
                .method(&method)
                .uri(uri.clone())
                .with_auth_string(auth)
                .body(body)
                .expect("Unable to build request")
        };

        log::trace!("Obtaining token from auth store");
        let token = self.auth_store.get_token().await;
        log::trace!("Token: {:?}", token);

        let request = build_request(&token);
        log::trace!("Request: {:?}. Sending request...", request);

        let mut response = self.client.request(request).await?;
        log::debug!("-> Response: {:}", response.status());

        match response.status() {
            StatusCode::OK => { /* cool */ }

            // 401: Unauthorized. Token may have expired or is invalid. Attempt to renew.
            StatusCode::UNAUTHORIZED => {
                if !retry_auth {
                    return Err(Error::ClientError("Unauthorized".into()));
                }

                if let Some(credentials) = &self.auth_store.get_credentials().await {
                    log::debug!(
                        "Renewing token using credentials: u: {:?}",
                        credentials.username
                    );
                    let new_token = self.authenticate(credentials.clone()).await?;

                    let request = build_request(&Some(new_token.to_string()));
                    response = self.client.request(request).await?;
                } else {
                    return Err(Error::ClientError(
                        "Unauthorized, no credentials provided".into(),
                    ));
                }
            }

            // Other errors: bubble up.
            _ => {
                let status = response.status();
                let body_str = hyper::body::to_bytes(response.into_body()).await?;
                let message = format!(
                    "Request failed ({:}). Response body: {:?}",
                    status,
                    String::from_utf8_lossy(&body_str)
                );
                return Err(Error::ClientError(message));
            }
        }

        Ok(response)
    }

    // Fetch an attachment while preserving response headers (e.g., Content-Type).
    // Returns the streaming body and the Content-Type header if present.
    pub async fn fetch_attachment_with_metadata(
        &mut self,
        guid: &str,
        preview: bool,
    ) -> Result<(ResponseStream, Option<String>), Error> {
        let endpoint = format!("attachment?guid={}&preview={}", guid, preview);
        let response = self
            .response_with_body_retry(&endpoint, Method::GET, Body::empty, true)
            .await?;

        let content_type = response
            .headers()
            .get(hyper::header::CONTENT_TYPE)
            .and_then(|v| v.to_str().ok())
            .map(|s| s.to_string());

        Ok((ResponseStream::from(response.into_body()), content_type))
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use crate::api::InMemoryAuthenticationStore;

    #[cfg(test)]
    fn local_mock_client() -> HTTPAPIClient<InMemoryAuthenticationStore> {
        let base_url = "http://localhost:5738".parse().unwrap();
        let credentials = Credentials {
            username: "test".to_string(),
            password: "test".to_string(),
        };

        HTTPAPIClient::new(
            base_url,
            InMemoryAuthenticationStore::new(Some(credentials)),
        )
    }

    #[cfg(test)]
    async fn mock_client_is_reachable() -> bool {
        let mut client = local_mock_client();
        let version = client.get_version().await;

        match version {
            Ok(_) => true,
            Err(e) => {
                log::error!("Mock client error: {:?}", e);
                false
            }
        }
    }

    #[tokio::test]
    async fn test_version() {
        if !mock_client_is_reachable().await {
            log::warn!("Skipping http_client tests (mock server not reachable)");
            return;
        }

        let mut client = local_mock_client();
        let version = client.get_version().await.unwrap();
        assert!(version.starts_with("KordophoneMock-"));
    }

    #[tokio::test]
    async fn test_conversations() {
        if !mock_client_is_reachable().await {
            log::warn!("Skipping http_client tests (mock server not reachable)");
            return;
        }

        let mut client = local_mock_client();
        let conversations = client.get_conversations().await.unwrap();
        assert!(!conversations.is_empty());
    }

    #[tokio::test]
    async fn test_messages() {
        if !mock_client_is_reachable().await {
            log::warn!("Skipping http_client tests (mock server not reachable)");
            return;
        }

        let mut client = local_mock_client();
        let conversations = client.get_conversations().await.unwrap();
        let conversation = conversations.first().unwrap();
        let messages = client
            .get_messages(&conversation.guid, None, None, None)
            .await
            .unwrap();
        assert!(!messages.is_empty());
    }

    #[tokio::test]
    async fn test_updates() {
        if !mock_client_is_reachable().await {
            log::warn!("Skipping http_client tests (mock server not reachable)");
            return;
        }

        let mut client = local_mock_client();

        // We just want to see if the connection is established, we won't wait for any events
        let _ = client.open_event_socket(None).await.unwrap();
        assert!(true);
    }
}
