// 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::collections::{BTreeMap, HashMap, HashSet, VecDeque};
use std::ops::ControlFlow;
use std::sync::Arc;

use datafusion::error::{DataFusionError, Result as DFResult};
use datafusion::sql::planner::IdentNormalizer;
use datafusion::sql::sqlparser::ast::{
    visit_expressions, visit_expressions_mut, Expr as SqlExpr, FunctionArg, FunctionArgExpr,
    FunctionArguments, Ident, ObjectName, Statement,
};
use datafusion::sql::sqlparser::dialect::GenericDialect;
use datafusion::sql::sqlparser::parser::Parser;
use paimon::catalog::{Catalog, Function, Identifier};

const MAX_EXPANSION_DEPTH: usize = 32;
const MAX_FUNCTION_REFERENCES: usize = 1024;
const MAX_EXPANDED_CALLS: usize = 1024;
type FunctionReference = (String, Identifier);

pub(crate) async fn expand_sql(
    sql: &str,
    catalogs: &HashMap<String, Arc<dyn Catalog>>,
    current_catalog: &str,
    current_database: &str,
) -> DFResult<String> {
    expand_sql_internal(sql, catalogs, current_catalog, current_database, None).await
}

async fn expand_sql_internal(
    sql: &str,
    catalogs: &HashMap<String, Arc<dyn Catalog>>,
    current_catalog: &str,
    current_database: &str,
    candidate: Option<&Function>,
) -> DFResult<String> {
    let mut statements = Parser::parse_sql(&GenericDialect {}, sql)
        .map_err(|error| DataFusionError::Plan(format!("Invalid REST SQL: {error}")))?;
    if statements.len() != 1 {
        return Err(DataFusionError::Plan(format!(
            "REST SQL must contain exactly one statement, found {}",
            statements.len()
        )));
    }
    let statement = expand_statement_internal(
        statements.remove(0),
        catalogs,
        current_catalog,
        current_database,
        MAX_EXPANDED_CALLS,
        candidate,
    )
    .await?;
    Ok(statement.to_string())
}

pub(crate) async fn expand_sql_with_candidate(
    sql: &str,
    catalogs: &HashMap<String, Arc<dyn Catalog>>,
    current_catalog: &str,
    current_database: &str,
    candidate: &Function,
) -> DFResult<String> {
    expand_sql_internal(
        sql,
        catalogs,
        current_catalog,
        current_database,
        Some(candidate),
    )
    .await
}

pub(crate) async fn expand_statement(
    statement: Statement,
    catalogs: &HashMap<String, Arc<dyn Catalog>>,
    current_catalog: &str,
    current_database: &str,
) -> DFResult<Statement> {
    expand_statement_with_budget(
        statement,
        catalogs,
        current_catalog,
        current_database,
        MAX_EXPANDED_CALLS,
    )
    .await
}

pub(crate) async fn expand_statement_with_budget(
    statement: Statement,
    catalogs: &HashMap<String, Arc<dyn Catalog>>,
    current_catalog: &str,
    current_database: &str,
    max_expanded_calls: usize,
) -> DFResult<Statement> {
    expand_statement_internal(
        statement,
        catalogs,
        current_catalog,
        current_database,
        max_expanded_calls,
        None,
    )
    .await
}

async fn expand_statement_internal(
    mut statement: Statement,
    catalogs: &HashMap<String, Arc<dyn Catalog>>,
    current_catalog: &str,
    current_database: &str,
    max_expanded_calls: usize,
    candidate: Option<&Function>,
) -> DFResult<Statement> {
    let candidate_reference =
        candidate.map(|function| (current_catalog.to_string(), function.identifier().clone()));
    let mut functions = HashMap::new();
    let mut dependencies = HashMap::new();
    let mut total_expanded_calls = 0;
    for _ in 0..MAX_EXPANSION_DEPTH {
        let mut references = BTreeMap::new();
        let _: ControlFlow<()> = visit_expressions(&statement, |expr| {
            if let SqlExpr::Function(function) = expr {
                if let Some(reference) =
                    function_reference(function, current_catalog, current_database)
                {
                    references.insert(reference, ());
                }
            }
            ControlFlow::Continue(())
        });

        let mut pending = references.into_keys().collect::<VecDeque<_>>();
        while let Some(reference) = pending.pop_front() {
            if functions.contains_key(&reference) {
                continue;
            }
            if functions.len() >= MAX_FUNCTION_REFERENCES {
                return Err(DataFusionError::Plan(format!(
                    "REST SQL function expansion exceeded the reference limit of {MAX_FUNCTION_REFERENCES}"
                )));
            }
            let Some(catalog) = catalogs.get(&reference.0) else {
                functions.insert(reference, None);
                continue;
            };
            let function = if candidate_reference.as_ref() == Some(&reference) {
                candidate.cloned()
            } else {
                match catalog.get_function(&reference.1).await {
                    Ok(function) => Some(function),
                    Err(paimon::Error::FunctionNotExist { .. })
                    | Err(paimon::Error::Unsupported { .. }) => None,
                    Err(error) => return Err(crate::to_datafusion_error(error)),
                }
            };
            if let Some(function) = &function {
                let nested = function_dependencies(function, &reference.0, reference.1.database());
                pending.extend(nested.iter().cloned());
                dependencies.insert(reference.clone(), nested);
            }
            functions.insert(reference, function);
        }

        if let Some(cycle) = find_dependency_cycle(&dependencies) {
            let path = cycle
                .iter()
                .map(|(catalog, identifier)| format!("{catalog}.{}", identifier.full_name()))
                .collect::<Vec<_>>()
                .join(" -> ");
            return Err(DataFusionError::Plan(format!(
                "recursive REST SQL function dependency detected: {path}"
            )));
        }

        let mut expanded_count = 0;
        let flow = visit_expressions_mut(&mut statement, |expr| {
            let SqlExpr::Function(call) = expr else {
                return ControlFlow::Continue(());
            };
            let Some(reference) = function_reference(call, current_catalog, current_database)
            else {
                return ControlFlow::Continue(());
            };
            let Some(Some(function)) = functions.get(&reference) else {
                return ControlFlow::Continue(());
            };
            if total_expanded_calls >= max_expanded_calls {
                return ControlFlow::Break(DataFusionError::Plan(format!(
                    "REST SQL function expansion budget of {max_expanded_calls} calls exceeded"
                )));
            }
            total_expanded_calls += 1;

            match expand_call(function, call, &reference.0, &functions) {
                Ok(expanded) => {
                    *expr = expanded;
                    expanded_count += 1;
                    ControlFlow::Continue(())
                }
                Err(error) => ControlFlow::Break(error),
            }
        });
        if let ControlFlow::Break(error) = flow {
            return Err(error);
        }
        if expanded_count == 0 {
            return Ok(statement);
        }
    }

    Err(DataFusionError::Plan(format!(
        "REST SQL function expansion exceeded maximum depth of {MAX_EXPANSION_DEPTH}"
    )))
}

fn function_dependencies(
    function: &Function,
    current_catalog: &str,
    current_database: &str,
) -> Vec<FunctionReference> {
    let Some(definition) = function
        .definition("datafusion")
        .and_then(paimon::catalog::FunctionDefinition::sql)
    else {
        return Vec::new();
    };
    let Ok(mut parser) = Parser::new(&GenericDialect {}).try_with_sql(definition) else {
        return Vec::new();
    };
    let Ok(body) = parser.parse_expr() else {
        return Vec::new();
    };
    let mut dependencies = Vec::new();
    let _: ControlFlow<()> = visit_expressions(&body, |expr| {
        if let SqlExpr::Function(call) = expr {
            if let Some(reference) = function_reference(call, current_catalog, current_database) {
                dependencies.push(reference);
            }
        }
        ControlFlow::Continue(())
    });
    dependencies
}

fn find_dependency_cycle(
    dependencies: &HashMap<FunctionReference, Vec<FunctionReference>>,
) -> Option<Vec<FunctionReference>> {
    fn visit(
        reference: &FunctionReference,
        dependencies: &HashMap<FunctionReference, Vec<FunctionReference>>,
        finished: &mut HashSet<FunctionReference>,
        path: &mut Vec<FunctionReference>,
    ) -> Option<Vec<FunctionReference>> {
        if let Some(start) = path.iter().position(|entry| entry == reference) {
            let mut cycle = path[start..].to_vec();
            cycle.push(reference.clone());
            return Some(cycle);
        }
        if finished.contains(reference) {
            return None;
        }

        path.push(reference.clone());
        if let Some(next_references) = dependencies.get(reference) {
            for next in next_references {
                if let Some(cycle) = visit(next, dependencies, finished, path) {
                    return Some(cycle);
                }
            }
        }
        path.pop();
        finished.insert(reference.clone());
        None
    }

    let mut finished = HashSet::new();
    for reference in dependencies.keys() {
        if let Some(cycle) = visit(reference, dependencies, &mut finished, &mut Vec::new()) {
            return Some(cycle);
        }
    }
    None
}

fn function_reference(
    function: &datafusion::sql::sqlparser::ast::Function,
    current_catalog: &str,
    current_database: &str,
) -> Option<(String, Identifier)> {
    let identifiers = function
        .name
        .0
        .iter()
        .map(|part| part.as_ident().map(normalize_identifier))
        .collect::<Option<Vec<_>>>()?;
    match identifiers.as_slice() {
        [function] => Some((
            current_catalog.to_string(),
            Identifier::new(current_database, function),
        )),
        [catalog, database, function] => {
            Some((catalog.clone(), Identifier::new(database, function)))
        }
        _ => None,
    }
}

fn normalize_identifier(identifier: &Ident) -> String {
    IdentNormalizer::default().normalize(identifier.clone())
}

fn expand_call(
    function: &Function,
    call: &datafusion::sql::sqlparser::ast::Function,
    owner_catalog: &str,
    functions: &HashMap<FunctionReference, Option<Function>>,
) -> DFResult<SqlExpr> {
    if !function.is_deterministic() {
        return Err(DataFusionError::Plan(format!(
            "REST SQL function '{}' is non-deterministic and is not supported",
            function.full_name()
        )));
    }
    if function
        .return_params()
        .map(<[paimon::spec::DataField]>::len)
        != Some(1)
    {
        return Err(DataFusionError::Plan(format!(
            "REST SQL function '{}' must declare exactly one return parameter",
            function.full_name()
        )));
    }
    let input_params = function.input_params().ok_or_else(|| {
        DataFusionError::Plan(format!(
            "REST SQL function '{}' has no input parameters",
            function.full_name()
        ))
    })?;
    let definition = function
        .definition("datafusion")
        .and_then(paimon::catalog::FunctionDefinition::sql)
        .ok_or_else(|| {
            DataFusionError::Plan(format!(
                "REST SQL function '{}' has no datafusion SQL definition",
                function.full_name()
            ))
        })?;

    let FunctionArguments::List(args) = &call.args else {
        return Err(DataFusionError::Plan(format!(
            "REST SQL function '{}' requires positional arguments",
            function.full_name()
        )));
    };
    let values = args
        .args
        .iter()
        .map(|arg| match arg {
            FunctionArg::Unnamed(FunctionArgExpr::Expr(expr)) => Ok(expr.clone()),
            _ => Err(DataFusionError::Plan(format!(
                "REST SQL function '{}' requires positional expression arguments",
                function.full_name()
            ))),
        })
        .collect::<DFResult<Vec<_>>>()?;
    if values.len() != input_params.len() {
        return Err(DataFusionError::Plan(format!(
            "REST SQL function '{}' expects {} arguments but received {}",
            function.full_name(),
            input_params.len(),
            values.len()
        )));
    }

    let mut body = Parser::new(&GenericDialect {})
        .try_with_sql(definition)
        .map_err(|error| {
            DataFusionError::Plan(format!("Invalid SQL function definition: {error}"))
        })?
        .parse_expr()
        .map_err(|error| {
            DataFusionError::Plan(format!("Invalid SQL function definition: {error}"))
        })?;
    let replacements: HashMap<String, SqlExpr> = input_params
        .iter()
        .zip(values)
        .map(|(field, value)| (field.name().to_string(), value))
        .collect();
    let validation = visit_expressions(&body, |expr| match expr {
        SqlExpr::Identifier(identifier)
            if !replacements.contains_key(&normalize_identifier(identifier)) =>
        {
            ControlFlow::Break(identifier.value.clone())
        }
        SqlExpr::CompoundIdentifier(identifiers) => ControlFlow::Break(
            identifiers
                .iter()
                .map(|identifier| identifier.value.as_str())
                .collect::<Vec<_>>()
                .join("."),
        ),
        _ => ControlFlow::Continue(()),
    });
    if let ControlFlow::Break(identifier) = validation {
        return Err(DataFusionError::Plan(format!(
            "REST SQL function '{}' references undeclared identifier '{identifier}'",
            function.full_name()
        )));
    }
    let _: ControlFlow<()> = visit_expressions_mut(&mut body, |expr| {
        let SqlExpr::Function(call) = expr else {
            return ControlFlow::Continue(());
        };
        let Some(function_name) = bare_function_name(call) else {
            return ControlFlow::Continue(());
        };
        let function_name = normalize_identifier(&function_name);
        let reference = (
            owner_catalog.to_string(),
            Identifier::new(function.identifier().database(), &function_name),
        );
        if matches!(functions.get(&reference), Some(Some(_))) {
            call.name = ObjectName::from(vec![
                Ident::with_quote('"', owner_catalog),
                Ident::with_quote('"', function.identifier().database()),
                Ident::with_quote('"', function_name),
            ]);
        }
        ControlFlow::Continue(())
    });
    let _: ControlFlow<()> = visit_expressions_mut(&mut body, |expr| {
        if let SqlExpr::Identifier(identifier) = expr {
            if let Some(replacement) = replacements.get(&normalize_identifier(identifier)) {
                *expr = replacement.clone();
            }
        }
        ControlFlow::Continue(())
    });

    let return_type = function.return_params().expect("validated above")[0].data_type();
    let sql_type = crate::table::data_type_to_sql(return_type).map_err(|error| {
        DataFusionError::Plan(format!(
            "Invalid return type for REST SQL function '{}': {error}",
            function.full_name()
        ))
    })?;
    Parser::new(&GenericDialect {})
        .try_with_sql(&format!("CAST(({body}) AS {sql_type})"))
        .map_err(|error| {
            DataFusionError::Plan(format!(
                "Invalid return type for REST SQL function '{}': {error}",
                function.full_name()
            ))
        })?
        .parse_expr()
        .map_err(|error| {
            DataFusionError::Plan(format!(
                "Invalid return type for REST SQL function '{}': {error}",
                function.full_name()
            ))
        })
}

fn bare_function_name(function: &datafusion::sql::sqlparser::ast::Function) -> Option<Ident> {
    match function.name.0.as_slice() {
        [part] => part.as_ident().cloned(),
        _ => None,
    }
}

#[cfg(test)]
mod tests {
    use std::collections::HashMap;

    use super::expand_sql;

    #[tokio::test]
    async fn leaves_bare_functions_for_datafusion_without_a_paimon_catalog() {
        let sql = "SELECT vector_from_json('[1, 2.5]')";

        let expanded = expand_sql(sql, &HashMap::new(), "datafusion", "public")
            .await
            .unwrap();

        assert_eq!(expanded, sql);
    }
}
