// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use anyhow::Error;
use async_trait::async_trait;
use futures::channel::oneshot;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::{fmt::Debug, str::FromStr};
use thiserror::Error;

use crate::server::constants::{COMMAND_DELIMITER, COMMAND_SIZE};

/// An Sl4f facade that can handle incoming requests.
#[async_trait(?Send)]
pub trait Facade: Debug {
    /// Asynchronously handle the incoming request for the given method and arguments, returning a
    /// future object representing the pending operation.
    async fn handle_request(&self, method: String, args: Value) -> Result<Value, Error>;

    /// In response to a request to /cleanup, cleanup any cross-request state.
    fn cleanup(&self) {}

    /// In response to a request to /print, log relevant facade state.
    fn print(&self) {}
}

/// Information about each client that has connected
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ClientData {
    // client_id: String ID of client (ACTS test suite)
    pub command_id: Value,

    // command_result: The response of running the command (to be stored in the table)
    pub command_result: AsyncResponse,
}

impl ClientData {
    pub fn new(id: Value, result: AsyncResponse) -> ClientData {
        ClientData { command_id: id, command_result: result }
    }
}

/// The parsed `id` field from an incoming json-rpc request.
#[derive(Debug, PartialEq, Clone)]
pub struct RequestId {
    /// If the request ID is a string that contains a single '.', the text leading up to the '.' is
    /// extracted as the client identifier.
    client: Option<String>,

    /// The ID to send in the response.  If client is Some(_), this will be a substring of the
    /// request ID.
    id: Value,
}

impl RequestId {
    /// Parse a raw request ID into its session id (if present) and response id.
    pub fn new(raw: Value) -> Self {
        if let Some(s) = raw.as_str() {
            let parts = s.split('.').collect::<Vec<_>>();
            if parts.len() == 2 {
                return Self {
                    client: Some(parts[0].to_owned()),
                    id: Value::String(parts[1..].join(".")),
                };
            }
        }

        // If the raw ID wasn't a string that contained exactly 1 '.', pass it through to the
        // response unmodified.
        Self { client: None, id: raw }
    }

    /// Returns a reference to the session id, if present.
    pub fn session_id(&self) -> Option<&str> {
        self.client.as_ref().map(String::as_str)
    }

    /// Returns a reference to the response id.
    pub fn response_id(&self) -> &Value {
        &self.id
    }

    /// Returns the response id, consuming self.
    pub fn into_response_id(self) -> Value {
        self.id
    }
}

/// The parsed `method` field from an incoming json-rpc request.
#[derive(Debug, PartialEq, Eq, Clone, Default)]
pub struct MethodId {
    /// Method type of the request (e.g bluetooth, wlan, etc...)
    pub facade: String,

    /// Name of the method
    pub method: String,
}

impl FromStr for MethodId {
    type Err = MethodIdParseError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let parts = s.split(COMMAND_DELIMITER).collect::<Vec<_>>();

        if parts.len() != COMMAND_SIZE {
            return Err(MethodIdParseError(s.to_string()));
        }

        Ok(Self { facade: parts[0].to_string(), method: parts[1].to_string() })
    }
}

#[derive(Debug, PartialEq, Eq, Clone, Error)]
#[error("invalid method id: {}", _0)]
pub struct MethodIdParseError(String);

/// Required fields for making a request
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct CommandRequest {
    // method: name of method to be called
    pub method: String,

    // id: String id of command
    pub id: Value,

    // params: Arguments required for method
    pub params: Value,
}

/// Return packet after SL4F runs command
#[derive(Serialize, Clone, Debug)]
pub struct CommandResponse {
    // id: String id of command
    pub id: Value,

    // result: Result value of method call, can be None
    pub result: Option<Value>,

    // error: Error message of method call, can be None
    pub error: Option<String>,
}

impl CommandResponse {
    pub fn new(id: Value, result: Option<Value>, error: Option<String>) -> CommandResponse {
        CommandResponse { id, result, error }
    }
}

/// Represents a RPC request to be fulfilled by the FIDL event loop
#[derive(Debug)]
pub enum AsyncRequest {
    Cleanup(oneshot::Sender<()>),
    Command(AsyncCommandRequest),
}

/// Represents a RPC command request to be fulfilled by the FIDL event loop
#[derive(Debug)]
pub struct AsyncCommandRequest {
    // tx: Transmit channel from FIDL event loop to RPC request side
    pub tx: oneshot::Sender<AsyncResponse>,

    // method_id: struct containing:
    //  * facade: Method type of the request (e.g bluetooth, wlan, etc...)
    //  * method: Name of the method
    pub method_id: MethodId,

    // params: serde_json::Value representing args for method
    pub params: Value,
}

impl AsyncCommandRequest {
    pub fn new(
        tx: oneshot::Sender<AsyncResponse>,
        method_id: MethodId,
        params: Value,
    ) -> AsyncCommandRequest {
        AsyncCommandRequest { tx, method_id, params }
    }
}

/// Represents a RPC response from the FIDL event loop to the RPC request side
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AsyncResponse {
    // res: serde_json::Value of FIDL method result
    pub result: Option<Value>,

    pub error: Option<String>,
}

impl AsyncResponse {
    pub fn new(res: Result<Value, Error>) -> AsyncResponse {
        match res {
            Ok(v) => AsyncResponse { result: Some(v), error: None },
            Err(e) => AsyncResponse { result: None, error: Some(e.to_string()) },
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn parse_method_id_ok() {
        assert_eq!(
            "bt.send".parse(),
            Ok(MethodId { facade: "bt".to_string(), method: "send".to_string() })
        );
        assert_eq!(
            "FooFacade.BarMethod".parse(),
            Ok(MethodId { facade: "FooFacade".to_string(), method: "BarMethod".to_string() })
        );
        assert_eq!(
            "EmptyMethod.".parse(),
            Ok(MethodId { facade: "EmptyMethod".to_string(), method: "".to_string() })
        );
        assert_eq!(
            ".EmptyFacade".parse(),
            Ok(MethodId { facade: "".to_string(), method: "EmptyFacade".to_string() })
        );
    }

    #[test]
    fn parse_method_id_invalid() {
        fn assert_parse_error(s: &str) {
            assert_eq!(s.parse::<MethodId>(), Err(MethodIdParseError(s.to_string())));
        }

        // Invalid command (should result in empty result)
        assert_parse_error("bluetooth_send");

        // Too many separators in command
        assert_parse_error("wlan.scan.start");

        // Empty command
        assert_parse_error("");

        // No separator
        assert_parse_error("BluetoothSend");

        // Invalid separator
        assert_parse_error("Bluetooth,Scan");
    }

    #[test]
    fn parse_request_id_int() {
        let id = RequestId::new(json!(42));
        assert_eq!(id, RequestId { client: None, id: json!(42) });
        assert_eq!(id.session_id(), None);
        assert_eq!(id.response_id(), &json!(42));
        assert_eq!(id.into_response_id(), json!(42));
    }

    #[test]
    fn parse_request_id_single_str() {
        assert_eq!(RequestId::new(json!("123")), RequestId { client: None, id: json!("123") });
    }

    #[test]
    fn parse_request_id_too_many_dots() {
        assert_eq!(RequestId::new(json!("1.2.3")), RequestId { client: None, id: json!("1.2.3") });
    }

    #[test]
    fn parse_request_id_with_session_id() {
        let id = RequestId::new(json!("12.34"));
        assert_eq!(id, RequestId { client: Some("12".to_string()), id: json!("34") });
        assert_eq!(id.session_id(), Some("12"));
        assert_eq!(id.response_id(), &json!("34"));
        assert_eq!(id.into_response_id(), json!("34"));
    }
}
