// Copyright (C) 2024, Fluendo S.A.
//      Author: Andoni Morales Alastruey <amorales@fluendo.com>
//
// Copyright (C) 2024, Asymptotic Inc.
//      Author: Sanchayan Maity <sanchayan@asymptotic.io>
//
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at
// <https://mozilla.org/MPL/2.0/>.
//
// SPDX-License-Identifier: MPL-2.0

use crate::quinnquicmeta::QuinnQuicMeta;
use crate::quinnquicquery::*;
use crate::utils::{
    client, client_endpoint, get_stats, make_socket_addr, server_endpoint, wait, Canceller,
    QuinnQuicEndpointConfig, WaitError, CONNECTION_CLOSE_CODE, CONNECTION_CLOSE_MSG, RUNTIME,
};
use crate::{common::*, utils};
use async_channel::{unbounded, Receiver, Sender};
use bytes::{buf, Bytes};
use futures::{future::BoxFuture, stream::FuturesUnordered, StreamExt};
use gst::{glib, prelude::*, subclass::prelude::*};
use gst_base::prelude::*;
use gst_base::subclass::base_src::CreateSuccess;
use gst_base::subclass::prelude::*;
use quinn::{Connection, ConnectionError, TransportConfig};
use rustls::server;
use std::borrow::Borrow;
use std::fmt::Error;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{LazyLock, Mutex};
use std::thread::{spawn, Builder, JoinHandle};
use tokio::net::lookup_host;
use tokio::sync::oneshot;
use web_transport_quinn::*;

const DATA_HANDLER_THREAD: &str = "data-handler";

static CAT: LazyLock<gst::DebugCategory> = LazyLock::new(|| {
    gst::DebugCategory::new(
        "quinnwtclientsrc",
        gst::DebugColorFlags::empty(),
        Some("Quinn WebTransport client source"),
    )
});

enum QuinnData {
    Datagram(Bytes),
    Stream(u64, Bytes),
    Closed(u64),
    Eos,
}

struct Started {
    session: Session,
    data_handler: Option<JoinHandle<()>>,
    // TODO: Use tokio channel
    //
    // We use async-channel to keep a clone of the receive channel around
    // for use in every `create` call. tokio's UnboundedReceiver does not
    // implement clone.
    data_rx: Option<Receiver<QuinnData>>,
    thread_quit: Option<oneshot::Sender<()>>,
}

#[derive(Default)]
enum State {
    #[default]
    Stopped,
    Started(Started),
}

#[derive(Debug)]
struct Settings {
    bind_address: String,
    bind_port: u16,
    certificate_file: Option<PathBuf>,
    keep_alive_interval: u64,
    secure_conn: bool,
    timeout: u32,
    transport_config: QuinnQuicTransportConfig,
    url: String,
}

impl Default for Settings {
    fn default() -> Self {
        let transport_config = QuinnQuicTransportConfig::default();
        Settings {
            bind_address: DEFAULT_BIND_ADDR.to_string(),
            bind_port: DEFAULT_BIND_PORT,
            certificate_file: None,
            keep_alive_interval: 0,
            secure_conn: DEFAULT_SECURE_CONNECTION,
            timeout: DEFAULT_TIMEOUT,
            transport_config,
            url: DEFAULT_ADDR.to_string(),
        }
    }
}

pub struct QuinnWebTransportClientSrc {
    settings: Mutex<Settings>,
    state: Mutex<State>,
    canceller: Mutex<utils::Canceller>,
}

impl Default for QuinnWebTransportClientSrc {
    fn default() -> Self {
        Self {
            settings: Mutex::new(Settings::default()),
            state: Mutex::new(State::default()),
            canceller: Mutex::new(utils::Canceller::default()),
        }
    }
}

impl GstObjectImpl for QuinnWebTransportClientSrc {}

impl ElementImpl for QuinnWebTransportClientSrc {
    fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
        static ELEMENT_METADATA: LazyLock<gst::subclass::ElementMetadata> = LazyLock::new(|| {
            gst::subclass::ElementMetadata::new(
                "Quinn WebTransport Client Source",
                "Source/Network/QUIC",
                "Receive data over the network via WebTransport",
                "Andoni Morales Alastruey <amorales@fluendo.com>",
            )
        });
        Some(&*ELEMENT_METADATA)
    }

    fn pad_templates() -> &'static [gst::PadTemplate] {
        static PAD_TEMPLATES: LazyLock<Vec<gst::PadTemplate>> = LazyLock::new(|| {
            let src_pad_template = gst::PadTemplate::new(
                "src",
                gst::PadDirection::Src,
                gst::PadPresence::Always,
                &gst::Caps::new_any(),
            )
            .unwrap();

            vec![src_pad_template]
        });

        PAD_TEMPLATES.as_ref()
    }

    fn change_state(
        &self,
        transition: gst::StateChange,
    ) -> Result<gst::StateChangeSuccess, gst::StateChangeError> {
        if transition == gst::StateChange::NullToReady {
            let settings = self.settings.lock().unwrap();

            /*
             * Fail the state change if a secure connection was requested but
             * no certificate path was provided.
             */
            if settings.secure_conn && settings.certificate_file.is_none() {
                gst::error!(
                    CAT,
                    imp = self,
                    "Certificate or private key file not provided for secure connection"
                );
                return Err(gst::StateChangeError);
            }
        }
        self.parent_change_state(transition)
    }
}

impl ObjectImpl for QuinnWebTransportClientSrc {
    fn constructed(&self) {
        self.parent_constructed();
        self.obj().set_format(gst::Format::Time);
        self.obj().set_live(true);
        self.obj().set_do_timestamp(true);
    }

    fn properties() -> &'static [glib::ParamSpec] {
        static PROPERTIES: LazyLock<Vec<glib::ParamSpec>> = LazyLock::new(|| {
            vec![
                glib::ParamSpecString::builder("certificate-file")
                    .nick("Certificate file")
                    .blurb("Path to certificate chain in single file")
                    .build(),
		glib::ParamSpecUInt64::builder("keep-alive-interval")
                    .nick("QUIC connection keep alive interval in ms")
                    .blurb("Keeps QUIC connection alive by periodically pinging the server. Value set in ms, 0 disables this feature")
		    .default_value(0)
                    .readwrite()
                    .build(),
                glib::ParamSpecUInt::builder("timeout")
                    .nick("Timeout")
                    .blurb("Value in seconds to timeout WebTransport endpoint requests (0 = No timeout).")
                    .maximum(3600)
                    .default_value(DEFAULT_TIMEOUT)
                    .readwrite()
                    .build(),
                glib::ParamSpecString::builder("url")
                    .nick("Server URL")
                    .blurb("URL of the HTTP/3 server to connect to.")
                    .build(),
                glib::ParamSpecBoxed::builder::<gst::Structure>("stats")
                    .nick("Connection statistics")
                    .blurb("Connection statistics")
                    .read_only()
                    .build(),
                glib::ParamSpecBoolean::builder("secure-connection")
                    .nick("Use secure connection.")
                    .blurb("Use certificates for QUIC connection. False: Insecure connection, True: Secure connection.")
                    .default_value(DEFAULT_SECURE_CONNECTION)
                    .build(),
            ]
        });

        PROPERTIES.as_ref()
    }

    fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
        let mut settings = self.settings.lock().unwrap();

        match pspec.name() {
            "certificate-file" => {
                let value: String = value.get().unwrap();
                settings.certificate_file = Some(value.into());
            }
            "keep-alive-interval" => {
                settings.keep_alive_interval = value.get().expect("type checked upstream");
            }
            "timeout" => {
                settings.timeout = value.get().expect("type checked upstream");
            }
            "secure-connection" => {
                settings.secure_conn = value.get().expect("type checked upstream");
            }
            "url" => {
                settings.url = value.get::<String>().expect("type checked upstream");
            }
            _ => unimplemented!(),
        }
    }

    fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
        let settings = self.settings.lock().unwrap();

        match pspec.name() {
            "certificate-file" => {
                let certfile = settings.certificate_file.as_ref();
                certfile.and_then(|file| file.to_str()).to_value()
            }
            "keep-alive-interval" => settings.keep_alive_interval.to_value(),
            "timeout" => settings.timeout.to_value(),
            "url" => settings.url.to_value(),
            "secure-connection" => settings.secure_conn.to_value(),
            "stats" => {
                let state = self.state.lock().unwrap();
                match *state {
                    State::Started(ref state) => get_stats(Some(state.session.stats())).to_value(),
                    State::Stopped => get_stats(None).to_value(),
                }
            }
            _ => unimplemented!(),
        }
    }
}

#[glib::object_subclass]
impl ObjectSubclass for QuinnWebTransportClientSrc {
    const NAME: &'static str = "GstQuinnWebTransportClientSrc";
    type Type = super::QuinnWebTransportClientSrc;
    type ParentType = gst_base::PushSrc;
}

impl BaseSrcImpl for QuinnWebTransportClientSrc {
    fn is_seekable(&self) -> bool {
        false
    }

    fn start(&self) -> Result<(), gst::ErrorMessage> {
        let settings = self.settings.lock().unwrap();
        let timeout = settings.timeout;
        drop(settings);

        let mut state = self.state.lock().unwrap();

        if let State::Started { .. } = *state {
            unreachable!("QuinnWebTransportClientSrc already started");
        }

        match wait(&self.canceller, self.init_session(), timeout) {
            Ok(Ok(sess)) => {
                let sess_clone = sess.clone();

                let (tx_quit, rx_quit): (oneshot::Sender<()>, oneshot::Receiver<()>) =
                    oneshot::channel();
                let (data_tx, data_rx): (Sender<QuinnData>, Receiver<QuinnData>) = unbounded();

                let self_ = self.ref_counted();
                let data_handler = Builder::new()
                    .name(DATA_HANDLER_THREAD.to_string())
                    .spawn(move || {
                        self_.handle_data(sess_clone, data_tx, rx_quit);
                        gst::debug!(CAT, imp = self_, "Data handler thread exit");
                    })
                    .unwrap();

                *state = State::Started(Started {
                    session: sess,
                    data_handler: Some(data_handler),
                    data_rx: Some(data_rx),
                    thread_quit: Some(tx_quit),
                });

                gst::info!(CAT, imp = self, "Started");

                Ok(())
            }
            Ok(Err(e)) | Err(e) => match e {
                WaitError::FutureAborted => {
                    gst::warning!(CAT, imp = self, "Connection aborted");
                    Ok(())
                }
                WaitError::FutureError(err) => {
                    gst::error!(CAT, imp = self, "Connection request failed: {}", err);
                    Err(gst::error_msg!(
                        gst::ResourceError::Failed,
                        ["Connection request failed: {}", err]
                    ))
                }
            },
        }
    }

    fn stop(&self) -> Result<(), gst::ErrorMessage> {
        let mut state = self.state.lock().unwrap();

        if let State::Started(ref mut state) = *state {
            if let Some(channel) = state.thread_quit.take() {
                gst::debug!(CAT, imp = self, "Signalling threads to exit");
                let _ = channel.send(());
            }

            gst::debug!(CAT, imp = self, "Joining data handler thread");
            if let Some(handle) = state.data_handler.take() {
                match handle.join() {
                    Ok(_) => gst::debug!(CAT, imp = self, "Joined data handler thread"),
                    Err(e) => {
                        gst::error!(CAT, imp = self, "Failed to join data handler thread: {e:?}")
                    }
                }
            }

            state
                .session
                .close(CONNECTION_CLOSE_CODE, CONNECTION_CLOSE_MSG.as_bytes());
        }

        *state = State::Stopped;

        Ok(())
    }

    fn unlock(&self) -> Result<(), gst::ErrorMessage> {
        let mut canceller = self.canceller.lock().unwrap();
        canceller.abort();
        Ok(())
    }

    fn unlock_stop(&self) -> Result<(), gst::ErrorMessage> {
        let mut canceller = self.canceller.lock().unwrap();
        if matches!(&*canceller, Canceller::Cancelled) {
            *canceller = Canceller::None;
        }
        Ok(())
    }
}

impl PushSrcImpl for QuinnWebTransportClientSrc {
    fn create(
        &self,
        _buffer: Option<&mut gst::BufferRef>,
    ) -> Result<CreateSuccess, gst::FlowError> {
        loop {
            // We do not want `create` to return when a stream is closed,
            // but, wait for one of the other streams to receive data.
            match self.get() {
                Ok(Some(QuinnData::Stream(stream_id, bytes))) => {
                    break Ok(self.create_buffer(bytes, Some(stream_id)));
                }
                Ok(Some(QuinnData::Datagram(bytes))) => {
                    break Ok(self.create_buffer(bytes, None));
                }
                Ok(Some(QuinnData::Eos)) => {
                    gst::debug!(CAT, imp = self, "End of stream");
                    break Err(gst::FlowError::Eos);
                }
                Ok(None) => {
                    gst::debug!(CAT, imp = self, "End of stream");
                    break Err(gst::FlowError::Eos);
                }
                Err(None) => {
                    gst::debug!(CAT, imp = self, "Flushing");
                    break Err(gst::FlowError::Flushing);
                }
                Err(Some(err)) => {
                    gst::error!(CAT, imp = self, "Could not GET: {}", err);
                    break Err(gst::FlowError::Error);
                }
                Ok(Some(QuinnData::Closed(stream_id))) => {
                    // Send custom downstream event for demuxer to close
                    // and remove the stream.
                    let srcpad = self.obj().static_pad("src").expect("source pad expected");
                    close_stream(&srcpad, stream_id);
                }
            }
        }
    }
}

impl QuinnWebTransportClientSrc {
    fn create_buffer(&self, bytes: Bytes, stream_id: Option<u64>) -> CreateSuccess {
        gst::trace!(
            CAT,
            imp = self,
            "Pushing buffer of {} bytes for stream: {stream_id:?}",
            bytes.len()
        );

        let mut buffer = gst::Buffer::from_slice(bytes);
        {
            let buffer = buffer.get_mut().unwrap();
            match stream_id {
                Some(id) => QuinnQuicMeta::add(buffer, id, false),
                None => QuinnQuicMeta::add(buffer, 0, true),
            };
        }

        CreateSuccess::NewBuffer(buffer.to_owned())
    }

    fn get(&self) -> Result<Option<QuinnData>, Option<gst::ErrorMessage>> {
        let settings = self.settings.lock().unwrap();
        let timeout = settings.timeout;
        drop(settings);

        let state = self.state.lock().unwrap();
        let rx_chan = match *state {
            State::Started(ref started) => started.data_rx.clone(),
            State::Stopped => {
                return Err(Some(gst::error_msg!(
                    gst::LibraryError::Failed,
                    ["Cannot get data before start"]
                )));
            }
        };
        drop(state);

        let rx_chan = rx_chan.expect("Channel must be valid here");

        match wait(&self.canceller, rx_chan.recv(), timeout) {
            Ok(Ok(bytes)) => Ok(Some(bytes)),
            Ok(Err(_)) => Ok(None),
            Err(e) => match e {
                WaitError::FutureAborted => {
                    gst::warning!(CAT, imp = self, "Read from stream request aborted");
                    Err(None)
                }
                WaitError::FutureError(e) => {
                    gst::error!(CAT, imp = self, "Failed to read from stream: {}", e);
                    Err(Some(e))
                }
            },
        }
    }

    async fn init_session(&self) -> Result<Session, WaitError> {
        let (url, mut endpoint_config) = {
            let settings = self.settings.lock().unwrap();

            let client_addr = make_socket_addr(
                format!("{}:{}", settings.bind_address, settings.bind_port).as_str(),
            )?;

            let url = url::Url::parse(&settings.url).map_err(|err| {
                WaitError::FutureError(gst::error_msg!(
                    gst::ResourceError::Failed,
                    ["Failed to parse URL: {}", err]
                ))
            })?;

            (
                url.clone(),
                QuinnQuicEndpointConfig {
                    server_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 4443), // This will be filled in correctly later
                    server_name: DEFAULT_SERVER_NAME.to_string(),
                    client_addr: Some(client_addr),
                    secure_conn: settings.secure_conn,
                    alpns: vec![HTTP3_ALPN.to_string()],
                    certificate_file: settings.certificate_file.clone(),
                    private_key_file: None,
                    keep_alive_interval: settings.keep_alive_interval,
                    transport_config: settings.transport_config,
                    with_client_auth: false,
                },
            )
        };

        let server_port = url.port().unwrap_or(443);

        let host = url.host_str().ok_or_else(|| {
            WaitError::FutureError(gst::error_msg!(
                gst::ResourceError::Failed,
                ["Cannot parse host for URL: {}", url.as_str()]
            ))
        })?;

        // Look up the DNS entry.
        let mut remotes = lookup_host((host, server_port)).await.map_err(|_| {
            WaitError::FutureError(gst::error_msg!(
                gst::ResourceError::Failed,
                ["Cannot resolve host name for URL: {}", url.as_str()]
            ))
        })?;

        // Use the first entry.
        endpoint_config.server_addr = match remotes.next() {
            Some(remote) => Ok(remote),
            None => Err(WaitError::FutureError(gst::error_msg!(
                gst::ResourceError::Failed,
                ["Cannot resolve host name for URL: {}", url.as_str()]
            ))),
        }?;

        let client = client(&endpoint_config).map_err(|err| {
            WaitError::FutureError(gst::error_msg!(
                gst::ResourceError::Failed,
                ["Failed to configure endpoint: {}", err]
            ))
        })?;

        let session = client.connect(&url).await.map_err(|err| {
            WaitError::FutureError(gst::error_msg!(
                gst::ResourceError::Failed,
                ["Failed to connect to server: {}", err]
            ))
        })?;

        gst::info!(
            CAT,
            imp = self,
            "Remote connection accepted: {}",
            session.remote_address()
        );

        Ok(session)
    }

    fn handle_connection_error(&self, session_err: SessionError) {
        match session_err {
            SessionError::ConnectionError(err) => {
                gst::error!(CAT, imp = self, "Connection error: {err:?}");
            }
            SessionError::WebTransportError(err) => {
                gst::error!(CAT, imp = self, "WebTransport error: {err:?}");
            }
            SessionError::SendDatagramError(err) => {
                gst::error!(CAT, imp = self, "Send Datagram error: {err:?}");
            }
        }
    }

    fn handle_data(
        &self,
        session: Session,
        sender: Sender<QuinnData>,
        receiver: oneshot::Receiver<()>,
    ) {
        // Unifies the Future return types
        enum QuinnFuture {
            Datagram(Bytes),
            StreamData(RecvStream, QuinnData),
            Stream(RecvStream, u64),
            Stop,
        }

        let blocksize = self.obj().blocksize() as usize;
        gst::info!(CAT, imp = self, "Using a blocksize of {blocksize} for read",);

        let stream_idx = AtomicU64::new(0);

        let incoming_stream = |sess: Session, sidx: u64| async move {
            match sess.accept_uni().await {
                Ok(recv_stream) => QuinnFuture::Stream(recv_stream, sidx),
                Err(err) => {
                    self.handle_connection_error(err);
                    QuinnFuture::Stop
                }
            }
        };

        let datagram = |sess: Session| async move {
            match sess.read_datagram().await {
                Ok(bytes) => QuinnFuture::Datagram(bytes),
                Err(err) => {
                    self.handle_connection_error(err);
                    QuinnFuture::Stop
                }
            }
        };

        let recv_stream = |mut s: RecvStream, stream_id: u64| async move {
            match s.read_chunk(blocksize, true).await {
                Ok(Some(chunk)) => {
                    QuinnFuture::StreamData(s, QuinnData::Stream(stream_id, chunk.bytes))
                }
                Ok(None) => QuinnFuture::StreamData(s, QuinnData::Closed(stream_id)),
                Err(err) => match err {
                    ReadError::ClosedStream => {
                        gst::debug!(CAT, "Stream closed: {stream_id}");
                        QuinnFuture::StreamData(s, QuinnData::Closed(stream_id))
                    }
                    ReadError::SessionError(err) => {
                        gst::error!(CAT, "Connection lost: {err:?}");
                        QuinnFuture::StreamData(s, QuinnData::Eos)
                    }
                    rerr => {
                        gst::error!(CAT, "Read error on stream {stream_id}: {rerr:?}");
                        QuinnFuture::StreamData(s, QuinnData::Eos)
                    }
                },
            }
        };

        let tx_send = |sender: Sender<QuinnData>, data: QuinnData| async move {
            if let Err(err) = sender.send(data).await {
                gst::error!(CAT, imp = self, "Error sending data: {err:?}");
            }
        };

        // TODO:
        // Decide if the ordering matters when we might have a STREAM
        // Close followed by a Connection Close almost immediately.
        let mut tasks: FuturesUnordered<BoxFuture<QuinnFuture>> = FuturesUnordered::new();

        tasks.push(Box::pin(datagram(session.clone())));

        let idx = stream_idx.load(Ordering::Relaxed);
        stream_idx.fetch_add(1, Ordering::Relaxed);

        tasks.push(Box::pin(incoming_stream(session.clone(), idx)));
        // We only ever expect to receive on this channel once, so we
        // need not push this in the loop below.
        tasks.push(Box::pin(async {
            let _ = receiver.await;
            gst::debug!(CAT, imp = self, "Quitting");
            QuinnFuture::Stop
        }));

        RUNTIME.block_on(async {
            while let Some(stream) = tasks.next().await {
                match stream {
                    QuinnFuture::Stop => {
                        tx_send(sender.clone(), QuinnData::Eos).await;
                        break;
                    }
                    QuinnFuture::StreamData(s, data) => match data {
                        d @ QuinnData::Stream(stream_id, _) => {
                            gst::trace!(CAT, imp = self, "Sending data for stream: {stream_id}");
                            tx_send(sender.clone(), d).await;
                            tasks.push(Box::pin(recv_stream(s, stream_id)));
                        }
                        eos @ QuinnData::Eos => {
                            tx_send(sender.clone(), eos).await;
                            drop(s);
                            break;
                        }
                        c @ QuinnData::Closed(stream_id) => {
                            gst::trace!(CAT, imp = self, "Stream closed: {stream_id}");
                            tx_send(sender.clone(), c).await;
                            drop(s);
                        }
                        QuinnData::Datagram(_) => unreachable!(),
                    },
                    QuinnFuture::Stream(s, stream_id) => {
                        gst::trace!(
                            CAT,
                            imp = self,
                            "Incoming stream connection, {stream_id}, {s:?}"
                        );
                        tasks.push(Box::pin(recv_stream(s, stream_id)));

                        let idx = stream_idx.load(Ordering::Relaxed);
                        stream_idx.fetch_add(1, Ordering::Relaxed);

                        tasks.push(Box::pin(incoming_stream(session.clone(), idx)));
                    }
                    QuinnFuture::Datagram(b) => {
                        gst::trace!(CAT, imp = self, "Received {} bytes on datagram", b.len());
                        tx_send(sender.clone(), QuinnData::Datagram(b)).await;
                        tasks.push(Box::pin(datagram(session.clone())));
                    }
                }
            }
        });

        gst::info!(CAT, imp = self, "Quit data handler thread");
    }
}
