/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

#![deny(warnings)]
#![deny(rust_2018_idioms)]
#![deny(clippy::all)]

mod errors;

use std::fmt;

pub use errors::PersistError;
use hyper::Body;
use hyper::Client;
use hyper::Method;
use hyper::Request;
use hyper_tls::HttpsConnector;
use serde::Deserialize;
use url::form_urlencoded;

#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum Response {
    Success { id: String },
    Error { error: ResponseError },
}

#[derive(Debug, Deserialize)]
struct ResponseError {
    message: String,
}

impl fmt::Display for ResponseError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(&self.message)
    }
}

pub async fn persist(
    document: &str,
    uri: &str,
    params: impl IntoIterator<Item = (&String, &String)>,
    extra_headers: impl IntoIterator<Item = (&String, &String)>,
) -> Result<String, PersistError> {
    let request_body = {
        let mut request_body = form_urlencoded::Serializer::new(String::new());
        for param in params {
            request_body.append_pair(param.0, param.1);
        }
        request_body.append_pair("text", document);
        request_body.finish()
    };

    let mut builder = Request::builder()
        .method(Method::POST)
        .uri(uri)
        .header("content-type", "application/x-www-form-urlencoded");
    for (k, v) in extra_headers {
        builder = builder.header(k, v);
    }
    let req =
        builder
            .body(Body::from(request_body))
            .map_err(|err| PersistError::NetworkCreateError {
                error: Box::new(err),
            })?;
    let https = HttpsConnector::new();
    let client = Client::builder().build(https);
    let res = client.request(req).await?;
    let bytes = hyper::body::to_bytes(res.into_body()).await?;
    let result: Response = serde_json::from_slice(&bytes)?;

    match result {
        Response::Success { id } => Ok(id),
        Response::Error { error } => Err(PersistError::ErrorResponse {
            message: error.message,
        }),
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use thiserror::Error;

#[derive(Debug, Error)]
pub enum Error {
    #[error("Unable to filter projects. Error details: \n{details}")]
    ProjectFilterError { details: String },

    #[error("Unable to run the relay language server. Error details: \n{details}")]
    LSPError { details: String },

    #[error("{0}")]
    ConfigError(relay_compiler::errors::Error),

    #[error("Unable to run relay compiler. Error details: \n{details}")]
    CompilerError { details: String },
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use std::env;
use std::env::current_dir;
use std::path::PathBuf;
use std::process::Command;
use std::sync::Arc;

use clap::ArgEnum;
use clap::Parser;
use common::ConsoleLogger;
use intern::string_key::Intern;
use intern::Lookup;
use log::error;
use log::info;
use relay_compiler::build_project::artifact_writer::ArtifactValidationWriter;
use relay_compiler::compiler::Compiler;
use relay_compiler::config::Config;
use relay_compiler::errors::Error as CompilerError;
use relay_compiler::FileSourceKind;
use relay_compiler::LocalPersister;
use relay_compiler::OperationPersister;
use relay_compiler::PersistConfig;
use relay_compiler::RemotePersister;
use relay_lsp::start_language_server;
use relay_lsp::DummyExtraDataProvider;
use schema::SDLSchema;
use schema_documentation::SchemaDocumentationLoader;
use simplelog::ColorChoice;
use simplelog::ConfigBuilder as SimpleLogConfigBuilder;
use simplelog::LevelFilter;
use simplelog::TermLogger;
use simplelog::TerminalMode;

mod errors;

use errors::Error;

#[derive(Parser)]
#[clap(
     name = "Relay Compiler",
     version = option_env!("CARGO_PKG_VERSION").unwrap_or("unknown"),
     about = "Compiles Relay files and writes generated files.",
     rename_all = "camel_case",
     args_conflicts_with_subcommands = true
 )]
struct Opt {
    #[clap(subcommand)]
    command: Option<Commands>,

    #[clap(flatten)]
    compile: CompileCommand,
}

#[derive(Parser)]
#[clap(
    rename_all = "camel_case",
    about = "Compiles Relay files and writes generated files."
)]
struct CompileCommand {
    /// Compile and watch for changes
    #[clap(long, short)]
    watch: bool,

    /// Compile only this project. You can pass this argument multiple times.
    /// to compile multiple projects. If excluded, all projects will be compiled.
    #[clap(name = "project", long, short)]
    projects: Vec<String>,

    /// Compile using this config file. If not provided, searches for a config in
    /// package.json under the `relay` key or `relay.config.json` files among other up
    /// from the current working directory.
    config: Option<PathBuf>,

    #[clap(flatten)]
    cli_config: CliConfig,

    /// Run the persister even if the query has not changed.
    #[clap(long)]
    repersist: bool,

    /// Verbosity level
    #[clap(long, arg_enum, default_value = "verbose")]
    output: OutputKind,

    /// Looks for pending changes and exits with non-zero code instead of
    /// writing to disk
    #[clap(long)]
    validate: bool,
}

#[derive(Parser)]
#[clap(
    about = "Run the language server. Used by IDEs.",
    rename_all = "camel_case"
)]
struct LspCommand {
    /// Run the LSP using this config file. If not provided, searches for a config in
    /// package.json under the `relay` key or `relay.config.json` files among other up
    /// from the current working directory.
    config: Option<PathBuf>,

    /// Verbosity level
    #[clap(long, arg_enum, default_value = "quiet-with-errors")]
    output: OutputKind,
}

#[derive(clap::Subcommand)]
enum Commands {
    Compiler(CompileCommand),
    Lsp(LspCommand),
}

#[derive(ArgEnum, Clone, Copy)]
enum OutputKind {
    Debug,
    Quiet,
    QuietWithErrors,
    Verbose,
}

#[derive(Parser)]
#[clap(rename_all = "camel_case")]
pub struct CliConfig {
    /// Path for the directory where to search for source code
    #[clap(long)]
    pub src: Option<String>,
    /// Path to schema file
    #[clap(long)]
    pub schema: Option<String>,
    /// Path to a directory, where the compiler should write artifacts
    #[clap(long)]
    pub artifact_directory: Option<String>,
}

impl CliConfig {
    fn is_defined(&self) -> bool {
        self.src.is_some() || self.schema.is_some() || self.artifact_directory.is_some()
    }

    fn get_config_string(self) -> String {
        let src = self.src.unwrap_or_else(|| "./src".into());
        let schema = self.schema.unwrap_or_else(|| "./path-to-schema".into());
        let artifact_directory = self.artifact_directory.map_or("".to_string(), |a| {
            format!("\n  \"artifactDirectory\": \"{}\",", a)
        });
        format!(
            r#"
 {{
   "src": "{}",
   "schema": "{}",{}
   "language": "javascript"
 }}"#,
            src, schema, artifact_directory
        )
    }
}

#[tokio::main]
async fn main() {
    let opt = Opt::parse();

    let command = opt.command.unwrap_or(Commands::Compiler(opt.compile));

    let result = match command {
        Commands::Compiler(command) => handle_compiler_command(command).await,
        Commands::Lsp(command) => handle_lsp_command(command).await,
    };

    match result {
        Ok(_) => info!("Done."),
        Err(err) => {
            error!("{}", err);
            std::process::exit(1);
        }
    }
}

fn get_config(config_path: Option<PathBuf>) -> Result<Config, Error> {
    match config_path {
        Some(config_path) => Config::load(config_path).map_err(Error::ConfigError),
        None => Config::search(&current_dir().expect("Unable to get current working directory."))
            .map_err(Error::ConfigError),
    }
}

fn configure_logger(output: OutputKind, terminal_mode: TerminalMode) {
    let log_level = match output {
        OutputKind::Debug => LevelFilter::Debug,
        OutputKind::Quiet => LevelFilter::Off,
        OutputKind::QuietWithErrors => LevelFilter::Error,
        OutputKind::Verbose => LevelFilter::Info,
    };

    let log_config = SimpleLogConfigBuilder::new()
        .set_time_level(LevelFilter::Off)
        .set_target_level(LevelFilter::Off)
        .set_location_level(LevelFilter::Off)
        .set_thread_level(LevelFilter::Off)
        .build();

    TermLogger::init(log_level, log_config, terminal_mode, ColorChoice::Auto).unwrap();
}

/// Update Config if the `project` flag is set
fn set_project_flag(config: &mut Config, projects: Vec<String>) -> Result<(), Error> {
    if projects.is_empty() {
        return Ok(());
    }

    for project_config in config.projects.values_mut() {
        project_config.enabled = false;
    }
    for selected_project in projects {
        let selected_project = selected_project.intern();

        if let Some(project_config) = config.projects.get_mut(&selected_project) {
            project_config.enabled = true;
        } else {
            return Err(Error::ProjectFilterError {
                details: format!(
                    "Project `{}` not found, available projects: {}.",
                    selected_project,
                    config
                        .projects
                        .keys()
                        .map(|name| name.lookup())
                        .collect::<Vec<_>>()
                        .join(", ")
                ),
            });
        }
    }

    return Ok(());
}

async fn handle_compiler_command(command: CompileCommand) -> Result<(), Error> {
    configure_logger(command.output, TerminalMode::Mixed);

    if command.cli_config.is_defined() {
        return Err(Error::ConfigError(CompilerError::ConfigError {
            details: format!(
                "\nPassing Relay compiler configuration is not supported. Please add `relay.config.json` file,\nor \"relay\" section to your `package.json` file.\n\nCompiler configuration JSON:{}",
                command.cli_config.get_config_string(),
            ),
        }));
    }

    let mut config = get_config(command.config)?;

    set_project_flag(&mut config, command.projects)?;

    if command.validate {
        config.artifact_writer = Box::new(ArtifactValidationWriter::default());
    }

    config.create_operation_persister = Some(Box::new(|project_config| {
        project_config.persist.as_ref().map(
            |persist_config| -> Box<dyn OperationPersister + Send + Sync> {
                match persist_config {
                    PersistConfig::Remote(remote_config) => {
                        Box::new(RemotePersister::new(remote_config.clone()))
                    }
                    PersistConfig::Local(local_config) => {
                        Box::new(LocalPersister::new(local_config.clone()))
                    }
                }
            },
        )
    }));

    config.file_source_config = if should_use_watchman() {
        FileSourceKind::Watchman
    } else {
        FileSourceKind::WalkDir
    };
    config.repersist_operations = command.repersist;

    if command.watch && !matches!(&config.file_source_config, FileSourceKind::Watchman) {
        panic!(
            "Cannot run relay in watch mode if `watchman` is not available (or explicitly disabled)."
        );
    }

    let compiler = Compiler::new(Arc::new(config), Arc::new(ConsoleLogger));

    if command.watch {
        compiler.watch().await.map_err(|err| Error::CompilerError {
            details: format!("{:?}", err),
        })?;
    } else {
        compiler
            .compile()
            .await
            .map_err(|err| Error::CompilerError {
                details: format!("{}", err),
            })?;
    }

    Ok(())
}

async fn handle_lsp_command(command: LspCommand) -> Result<(), Error> {
    configure_logger(command.output, TerminalMode::Stderr);

    let config = get_config(command.config)?;

    let perf_logger = Arc::new(ConsoleLogger);
    let extra_data_provider = Box::new(DummyExtraDataProvider::new());
    let schema_documentation_loader: Option<Box<dyn SchemaDocumentationLoader<SDLSchema>>> = None;
    let js_language_server = None;

    start_language_server(
        config,
        perf_logger,
        extra_data_provider,
        schema_documentation_loader,
        js_language_server,
    )
    .await
    .map_err(|err| Error::LSPError {
        details: format!("Relay LSP unexpectedly terminated: {:?}", err),
    })?;

    info!("Relay LSP exited successfully.");

    Ok(())
}

/// Check if `watchman` is available.
/// Additionally, this method is checking for an existence of `FORCE_NO_WATCHMAN`
/// environment variable. If this `FORCE_NO_WATCHMAN` is set, this method will return `false`
/// and compiler will use non-watchman file finder.
fn should_use_watchman() -> bool {
    let check_watchman = Command::new("watchman")
        .args(["list-capabilities"])
        .output();

    check_watchman.is_ok() && env::var("FORCE_NO_WATCHMAN").is_err()
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use fnv::FnvBuildHasher;
use graphql_ir::ExecutableDefinitionName;
use graphql_syntax::FloatValue;
use graphql_syntax::OperationKind;
use indexmap::IndexSet;
use intern::string_key::StringKey;
use relay_config::DynamicModuleProvider;

#[derive(Eq, PartialEq, Hash, Debug)]
pub struct ObjectEntry {
    pub key: StringKey,
    pub value: Primitive,
}

/// A helper for creating Vec<ObjectEntry>
/// For now, field names are defined in `CODEGEN_CONSTANTS
#[macro_export]
macro_rules! object {
    { $ ( $(:$func: expr,)* $key:ident: $value:expr,)* } => ({
        use crate::constants::CODEGEN_CONSTANTS;
        vec![
            $(
                $(
                    $func,
                )*
                ObjectEntry {
                    key: CODEGEN_CONSTANTS.$key,
                    value: $value,
                },
            )*
        ]
    })
}

/// An interned codegen AST
#[derive(Eq, PartialEq, Hash, Debug)]
pub enum Ast {
    Object(Vec<ObjectEntry>),
    Array(Vec<Primitive>),
}

impl Ast {
    pub fn assert_object(&self) -> &[ObjectEntry] {
        match self {
            Ast::Object(result) => result,
            Ast::Array(_) => panic!("Expected an object"),
        }
    }

    pub fn assert_array(&self) -> &[Primitive] {
        match self {
            Ast::Object(_) => panic!("Expected an array"),
            Ast::Array(result) => result,
        }
    }
}

#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub enum ModuleImportName {
    Default(StringKey),
    Named {
        name: StringKey,
        import_as: Option<StringKey>,
    },
}

#[derive(Eq, PartialEq, Hash, PartialOrd, Ord, Debug, Clone)]
pub struct JSModuleDependency {
    pub path: StringKey,
    pub import_name: ModuleImportName,
}

#[derive(Eq, PartialEq, Hash, PartialOrd, Ord, Debug, Clone)]
pub enum GraphQLModuleDependency {
    Name(ExecutableDefinitionName),
    Path {
        name: ExecutableDefinitionName,
        path: StringKey,
    },
}

#[derive(Eq, PartialEq, Hash, Debug)]
pub enum Primitive {
    Key(AstKey),
    Variable(StringKey),
    String(StringKey),
    Float(FloatValue),
    Int(i64),
    Bool(bool),
    Null,
    StorageKey(StringKey, AstKey),
    RawString(String),
    GraphQLModuleDependency(GraphQLModuleDependency),
    JSModuleDependency(JSModuleDependency),

    // Don't include the value in the output when
    // skip_printing_nulls is enabled
    SkippableNull,
    DynamicImport {
        provider: DynamicModuleProvider,
        module: StringKey,
    },
    RelayResolverModel {
        graphql_module_name: StringKey,
        graphql_module_path: StringKey,
        js_module: JSModuleDependency,
        injected_field_name_details: Option<(StringKey, bool)>,
    },
    RelayResolverWeakObjectWrapper {
        resolver: Box<Primitive>,
        key: StringKey,
        plural: bool,
        live: bool,
    },
}

impl Primitive {
    pub fn assert_string(&self) -> StringKey {
        if let Primitive::String(key) = self {
            *key
        } else {
            panic!("Expected a string");
        }
    }

    pub fn assert_key(&self) -> AstKey {
        if let Primitive::Key(key) = self {
            *key
        } else {
            panic!("Expected a key");
        }
    }

    pub fn string_or_null(str: Option<StringKey>) -> Primitive {
        match str {
            None => Primitive::Null,
            Some(str) => Primitive::String(str),
        }
    }
}

#[derive(Eq, PartialEq, Hash, Copy, Clone, Debug)]
pub struct AstKey(usize);

impl AstKey {
    pub fn as_usize(self) -> usize {
        self.0
    }
}

#[derive(Default)]
pub struct AstBuilder {
    table: IndexSet<Ast, FnvBuildHasher>,
}

impl AstBuilder {
    pub fn intern(&mut self, ast: Ast) -> AstKey {
        AstKey(self.table.insert_full(ast).0)
    }

    pub fn lookup(&self, key: AstKey) -> &Ast {
        self.table.get_index(key.as_usize()).unwrap()
    }
}

#[derive(Clone)]
pub enum QueryID {
    Persisted { id: String, text_hash: String },
    External(StringKey),
}

pub struct RequestParameters<'a> {
    pub id: &'a Option<QueryID>,
    pub name: StringKey,
    pub operation_kind: OperationKind,
    pub text: Option<String>,
}

impl<'a> RequestParameters<'a> {
    pub fn is_client_request(&self) -> bool {
        self.id.is_none() && self.text.is_none()
    }
}