// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

use std::path::{Path, PathBuf};
use std::sync::Arc;

use datafusion::error::Result as DataFusionResult;
use datafusion::execution::runtime_env::RuntimeEnvBuilder;
use datafusion::execution::SessionStateBuilder;
use paimon::{CatalogOptions, FileSystemCatalog, Options};
use paimon_datafusion::SQLContext;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BenchmarkRuntimeConfig {
    pub target_partitions: usize,
    #[serde(default = "default_batch_size")]
    pub batch_size: usize,
    #[serde(default)]
    pub parquet_pushdown_filters: bool,
    pub memory_limit_bytes: Option<usize>,
    pub spill_dir: Option<PathBuf>,
    pub max_spill_bytes: Option<u64>,
}

impl Default for BenchmarkRuntimeConfig {
    fn default() -> Self {
        Self {
            target_partitions: std::thread::available_parallelism()
                .map(usize::from)
                .unwrap_or(1),
            batch_size: default_batch_size(),
            parquet_pushdown_filters: false,
            memory_limit_bytes: None,
            spill_dir: None,
            max_spill_bytes: None,
        }
    }
}

fn default_batch_size() -> usize {
    8192
}

pub fn build_sql_context(config: &BenchmarkRuntimeConfig) -> DataFusionResult<SQLContext> {
    let mut runtime = RuntimeEnvBuilder::new();
    if let Some(memory_limit) = config.memory_limit_bytes {
        runtime = runtime.with_memory_limit(memory_limit, 1.0);
    }
    if let Some(spill_dir) = &config.spill_dir {
        runtime = runtime.with_temp_file_path(spill_dir);
    }
    if let Some(max_spill_bytes) = config.max_spill_bytes {
        runtime = runtime.with_max_temp_directory_size(max_spill_bytes);
    }
    let sql = SQLContext::new();
    let state_ref = sql.ctx().state_ref();
    let current_state = state_ref.read().clone();
    let mut session_config = current_state
        .config()
        .clone()
        .with_target_partitions(config.target_partitions.max(1))
        .with_batch_size(config.batch_size.max(1));
    session_config
        .options_mut()
        .execution
        .parquet
        .pushdown_filters = config.parquet_pushdown_filters;
    let state = SessionStateBuilder::from(current_state)
        .with_config(session_config)
        .with_runtime_env(Arc::new(runtime.build()?))
        .build();
    *state_ref.write() = state;
    drop(state_ref);
    Ok(sql)
}

pub struct CatalogSession {
    pub sql: SQLContext,
    pub catalog: Arc<FileSystemCatalog>,
    pub catalog_name: String,
    pub database: String,
}

pub async fn open_catalog_session(
    runtime_config: &BenchmarkRuntimeConfig,
    warehouse: &Path,
    database: &str,
) -> Result<CatalogSession, Box<dyn std::error::Error + Send + Sync>> {
    std::fs::create_dir_all(warehouse)?;
    let mut options = Options::new();
    options.set(CatalogOptions::WAREHOUSE, warehouse.display().to_string());
    let catalog = Arc::new(FileSystemCatalog::new(options)?);
    let mut sql = build_sql_context(runtime_config)?;
    sql.register_catalog_with_default_db("paimon", catalog.clone(), Some(database))
        .await?;
    // Keep decoder batch boundaries identical for Parquet and Paimon. Paimon
    // retains Java's table default, so use its supported session override.
    sql.sql(&format!(
        "SET 'paimon.read.batch-size' = '{}'",
        runtime_config.batch_size
    ))
    .await?;
    Ok(CatalogSession {
        sql,
        catalog,
        catalog_name: "paimon".to_string(),
        database: database.to_string(),
    })
}
