// 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.

//! UPDATE execution for Paimon tables.
//!
//! Supports two execution paths:
//! - **Data evolution tables**: partial-column writes via [`paimon::table::TableUpdate`].
//! - **Append-only tables** (no PK, no deletion vectors): copy-on-write file rewriting
//!   via [`paimon::table::CopyOnWriteMergeWriter`].

use std::sync::Arc;

use datafusion::arrow::array::{Array, RecordBatch};
use datafusion::arrow::datatypes::{Field, Schema};
use datafusion::error::{DataFusionError, Result as DFResult};
use datafusion::prelude::{DataFrame, SessionContext};
use datafusion::sql::sqlparser::ast::{AssignmentTarget, TableFactor, Update};

use paimon::spec::CoreOptions;
use paimon::table::{CopyOnWriteMergeWriter, Table};

use crate::error::to_datafusion_error;
use crate::merge_into::{
    build_partition_set_from_where, extract_tracking_columns, is_delete_conflict,
    is_row_id_conflict, ok_result, project_update_columns, quote_identifier,
    register_cow_target_table, retry_on_conflict, TempTableTracker,
};
use crate::sql_context::SQLContext;

/// Execute an UPDATE statement on a Paimon table.
pub(crate) async fn execute_update(
    ctx: &SQLContext,
    update: &Update,
    table: Table,
) -> DFResult<DataFrame> {
    if let TableFactor::Table { alias: Some(a), .. } = &update.table.relation {
        return Err(DataFusionError::Plan(format!(
            "Table alias '{}' in UPDATE is not yet supported",
            a.name.value
        )));
    }

    let schema = table.schema();
    let core_options = CoreOptions::new(schema.options());

    if core_options.data_evolution_enabled() {
        execute_data_evolution_update(ctx, update, table).await
    } else if schema.trimmed_primary_keys().is_empty() {
        execute_cow_update(ctx, update, &table).await
    } else {
        Err(DataFusionError::Plan(
            "UPDATE on primary-key tables without data-evolution is not supported".to_string(),
        ))
    }
}

// ---------------------------------------------------------------------------
// Data evolution path
// ---------------------------------------------------------------------------

/// Execute UPDATE on a data evolution table with retry on row ID conflict.
async fn execute_data_evolution_update(
    ctx: &SQLContext,
    update: &Update,
    table: Table,
) -> DFResult<DataFrame> {
    retry_on_conflict("UPDATE", is_row_id_conflict, || {
        execute_update_once(ctx, update, &table)
    })
    .await
}

/// Single attempt of UPDATE execution.
async fn execute_update_once(
    ctx: &SQLContext,
    update: &Update,
    table: &Table,
) -> DFResult<DataFrame> {
    // 1. Extract SET assignments
    let mut columns = Vec::new();
    let mut exprs = Vec::new();
    for assignment in &update.assignments {
        let col_name = match &assignment.target {
            AssignmentTarget::ColumnName(name) => name
                .0
                .last()
                .and_then(|p| p.as_ident())
                .map(|id| id.value.clone())
                .ok_or_else(|| {
                    DataFusionError::Plan(format!("Invalid column name in SET: {name}"))
                })?,
            AssignmentTarget::Tuple(_) => {
                return Err(DataFusionError::Plan(
                    "Tuple assignment in UPDATE SET is not supported".to_string(),
                ));
            }
        };
        columns.push(col_name);
        exprs.push(assignment.value.to_string());
    }

    // 2. Create TableUpdate through the table write builder (validates preconditions)
    let wb = table.new_write_builder();
    let mut table_update = wb
        .new_update(columns.clone())
        .map_err(to_datafusion_error)?;

    // 3. Query the target table directly with WHERE filter.
    let table_ref = update.table.to_string();

    let select_parts: Vec<String> =
        std::iter::once("\"_ROW_ID\"".to_string())
            .chain(columns.iter().zip(exprs.iter()).map(|(col, expr)| {
                format!("{expr} AS {}", quote_identifier(&format!("__upd_{col}")))
            }))
            .collect();

    let select_clause = select_parts.join(", ");
    let where_clause = match &update.selection {
        Some(expr) => format!(" WHERE {expr}"),
        None => String::new(),
    };

    let query_sql = format!("SELECT {select_clause} FROM {table_ref}{where_clause}");
    let batches = ctx.ctx().sql(&query_sql).await?.collect().await?;

    // 4. Project update columns (rename __upd_X → X)
    let total_count: u64 = batches.iter().map(|b| b.num_rows() as u64).sum();
    if total_count == 0 {
        return ok_result(ctx.ctx(), 0);
    }

    let update_batches = project_update_columns(&batches, &columns)?;
    for batch in update_batches {
        table_update
            .add_matched_batch(batch)
            .map_err(to_datafusion_error)?;
    }

    // 5. Commit
    let messages = table_update
        .prepare_commit()
        .await
        .map_err(to_datafusion_error)?;
    if !messages.is_empty() {
        wb.try_new_commit()
            .map_err(to_datafusion_error)?
            .commit(messages)
            .await
            .map_err(to_datafusion_error)?;
    }

    ok_result(ctx.ctx(), total_count)
}

// ---------------------------------------------------------------------------
// Copy-on-Write path (append-only tables, no PK)
// ---------------------------------------------------------------------------

/// Execute UPDATE on an append-only table with retry on delete conflict.
async fn execute_cow_update(
    ctx: &SQLContext,
    update: &Update,
    table: &Table,
) -> DFResult<DataFrame> {
    retry_on_conflict("CoW UPDATE", is_delete_conflict, || {
        execute_cow_update_once(ctx, update, table)
    })
    .await
}

/// Single attempt of CoW UPDATE execution.
async fn execute_cow_update_once(
    ctx: &SQLContext,
    update: &Update,
    table: &Table,
) -> DFResult<DataFrame> {
    let (columns, exprs) = extract_set_assignments(update)?;

    let table_ref = update.table.to_string();
    let where_str = update.selection.as_ref().map(|e| e.to_string());
    let partition_set =
        build_partition_set_from_where(ctx, table, &table_ref, where_str.as_deref()).await?;

    let mut writer = CopyOnWriteMergeWriter::new(table, columns.clone(), partition_set)
        .await
        .map_err(to_datafusion_error)?;

    let mut temp_tracker = TempTableTracker::new(ctx);
    let (has_data, cow_table_name) =
        register_cow_target_table(ctx, table, &writer, &mut temp_tracker).await?;
    if !has_data {
        return ok_result(ctx.ctx(), 0);
    }

    let cow_target_name = cow_table_name;
    let result = execute_cow_update_inner(
        ctx.ctx(),
        &columns,
        &exprs,
        &cow_target_name,
        update,
        &mut writer,
    )
    .await;
    let total_count = result?;

    let messages = writer.prepare_commit().await.map_err(to_datafusion_error)?;
    if !messages.is_empty() {
        let commit = table
            .new_write_builder()
            .try_new_commit()
            .map_err(to_datafusion_error)?;
        commit.commit(messages).await.map_err(to_datafusion_error)?;
    }

    ok_result(ctx.ctx(), total_count)
}

async fn execute_cow_update_inner(
    ctx: &SessionContext,
    columns: &[String],
    exprs: &[String],
    cow_table_name: &str,
    update: &Update,
    writer: &mut CopyOnWriteMergeWriter,
) -> DFResult<u64> {
    let select_parts: Vec<String> =
        std::iter::once("\"__paimon_file_idx\"".to_string())
            .chain(std::iter::once("\"__paimon_row_offset\"".to_string()))
            .chain(columns.iter().zip(exprs.iter()).map(|(col, expr)| {
                format!("{expr} AS {}", quote_identifier(&format!("__upd_{col}")))
            }))
            .collect();

    let select_clause = select_parts.join(", ");
    let where_clause = match &update.selection {
        Some(expr) => format!(" WHERE {expr}"),
        None => String::new(),
    };

    // Safety: where_clause comes from sqlparser AST to_string(), not raw user input.
    let query_sql = format!("SELECT {select_clause} FROM {cow_table_name}{where_clause}");
    let join_result = ctx.sql(&query_sql).await?.collect().await?;

    let mut update_value_batches: Vec<RecordBatch> = Vec::new();
    let mut batch_counter: usize = 0;
    let mut total_count: u64 = 0;

    for batch in &join_result {
        if batch.num_rows() == 0 {
            continue;
        }

        let (file_idx_col, row_offset_col) = extract_tracking_columns(batch)?;

        let mut upd_fields = Vec::new();
        let mut upd_columns: Vec<Arc<dyn Array>> = Vec::new();
        for col in columns {
            let prefixed = format!("__upd_{col}");
            let idx = batch.schema().index_of(&prefixed).map_err(|e| {
                DataFusionError::Internal(format!("Column {prefixed} not found: {e}"))
            })?;
            upd_fields.push(Field::new(
                col,
                batch.schema().field(idx).data_type().clone(),
                true,
            ));
            upd_columns.push(batch.column(idx).clone());
        }
        let upd_schema = Arc::new(Schema::new(upd_fields));
        let upd_batch = RecordBatch::try_new(upd_schema, upd_columns)?;

        let current_batch_idx = batch_counter;
        update_value_batches.push(upd_batch);
        batch_counter += 1;

        for row in 0..batch.num_rows() {
            let file_idx = file_idx_col.value(row) as usize;
            let row_offset = row_offset_col.value(row) as usize;
            writer.add_matched_update(file_idx, row_offset, current_batch_idx, row);
            total_count += 1;
        }
    }

    if !update_value_batches.is_empty() {
        writer.set_update_batches(update_value_batches);
    }

    Ok(total_count)
}

/// Extract column names and expressions from UPDATE SET assignments.
fn extract_set_assignments(update: &Update) -> DFResult<(Vec<String>, Vec<String>)> {
    let mut columns = Vec::new();
    let mut exprs = Vec::new();
    for assignment in &update.assignments {
        let col_name = match &assignment.target {
            AssignmentTarget::ColumnName(name) => name
                .0
                .last()
                .and_then(|p| p.as_ident())
                .map(|id| id.value.clone())
                .ok_or_else(|| {
                    DataFusionError::Plan(format!("Invalid column name in SET: {name}"))
                })?,
            AssignmentTarget::Tuple(_) => {
                return Err(DataFusionError::Plan(
                    "Tuple assignment in UPDATE SET is not supported".to_string(),
                ));
            }
        };
        columns.push(col_name);
        exprs.push(assignment.value.to_string());
    }
    Ok((columns, exprs))
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::Arc;

    use datafusion::arrow::array::{Int32Array, StringViewArray, UInt64Array};
    use datafusion::sql::sqlparser::dialect::GenericDialect;
    use datafusion::sql::sqlparser::parser::Parser;
    use paimon::catalog::{Catalog, Identifier};
    use paimon::io::FileIOBuilder;
    use paimon::spec::{DataType, IntType, Schema as PaimonSchema, TableSchema};
    use paimon::{CatalogOptions, FileSystemCatalog, Options};
    use tempfile::TempDir;

    async fn setup_sql_context() -> (TempDir, SQLContext, Arc<FileSystemCatalog>) {
        let temp_dir = TempDir::new().unwrap();
        let warehouse = format!("file://{}", temp_dir.path().display());
        let mut options = Options::new();
        options.set(CatalogOptions::WAREHOUSE, warehouse);
        let catalog = Arc::new(FileSystemCatalog::new(options).unwrap());

        let mut sql_context = SQLContext::new();
        sql_context
            .register_catalog("paimon", catalog.clone())
            .await
            .unwrap();
        sql_context
            .sql("CREATE SCHEMA paimon.test_db")
            .await
            .unwrap();

        (temp_dir, sql_context, catalog)
    }

    async fn setup_data_evolution_table(name: &str) -> (TempDir, SQLContext, Table) {
        let (tmp, sql_context, catalog) = setup_sql_context().await;

        sql_context
            .sql(&format!(
                "CREATE TABLE paimon.test_db.{name} (id INT, name VARCHAR, value INT) WITH ('row-tracking.enabled' = 'true', 'data-evolution.enabled' = 'true')"
            ))
            .await
            .unwrap();

        sql_context
            .sql(&format!(
                "INSERT INTO paimon.test_db.{name} (id, name, value) VALUES (1, 'alice', 10), (2, 'bob', 20), (3, 'charlie', 30)"
            ))
            .await
            .unwrap()
            .collect()
            .await
            .unwrap();

        let table = catalog
            .get_table(&Identifier::new("test_db", name))
            .await
            .unwrap();

        (tmp, sql_context, table)
    }

    fn parse_update(sql: &str) -> Update {
        let dialect = GenericDialect {};
        let stmts = Parser::parse_sql(&dialect, sql).unwrap();
        match stmts.into_iter().next().unwrap() {
            datafusion::sql::sqlparser::ast::Statement::Update(u) => u,
            _ => panic!("Expected UPDATE statement"),
        }
    }

    fn collect_rows(batches: &[datafusion::arrow::array::RecordBatch]) -> Vec<(i32, String, i32)> {
        let mut rows = Vec::new();
        for batch in batches {
            let ids = batch
                .column(0)
                .as_any()
                .downcast_ref::<Int32Array>()
                .unwrap();
            let names = batch
                .column(1)
                .as_any()
                .downcast_ref::<StringViewArray>()
                .unwrap();
            let values = batch
                .column(2)
                .as_any()
                .downcast_ref::<Int32Array>()
                .unwrap();
            for i in 0..batch.num_rows() {
                rows.push((ids.value(i), names.value(i).to_string(), values.value(i)));
            }
        }
        rows
    }

    #[tokio::test]
    async fn test_update_with_where() {
        let (_tmp, sql_context, table) = setup_data_evolution_table("t_with_where").await;

        let update =
            parse_update("UPDATE paimon.test_db.t_with_where SET name = 'ALICE' WHERE id = 1");
        execute_update(&sql_context, &update, table).await.unwrap();

        let batches = sql_context
            .sql("SELECT id, name, value FROM paimon.test_db.t_with_where ORDER BY id")
            .await
            .unwrap()
            .collect()
            .await
            .unwrap();

        let rows = collect_rows(&batches);
        assert_eq!(
            rows,
            vec![
                (1, "ALICE".to_string(), 10),
                (2, "bob".to_string(), 20),
                (3, "charlie".to_string(), 30),
            ]
        );
    }

    #[tokio::test]
    async fn test_update_without_where() {
        let (_tmp, sql_context, table) = setup_data_evolution_table("t_without_where").await;

        let update = parse_update("UPDATE paimon.test_db.t_without_where SET value = 99");
        execute_update(&sql_context, &update, table).await.unwrap();

        let batches = sql_context
            .sql("SELECT id, name, value FROM paimon.test_db.t_without_where ORDER BY id")
            .await
            .unwrap()
            .collect()
            .await
            .unwrap();

        let rows = collect_rows(&batches);
        assert_eq!(
            rows,
            vec![
                (1, "alice".to_string(), 99),
                (2, "bob".to_string(), 99),
                (3, "charlie".to_string(), 99),
            ]
        );
    }

    #[tokio::test]
    async fn test_update_multiple_columns() {
        let (_tmp, sql_context, table) = setup_data_evolution_table("t_multi_col").await;

        let update = parse_update(
            "UPDATE paimon.test_db.t_multi_col SET name = 'updated', value = 0 WHERE id = 2",
        );
        execute_update(&sql_context, &update, table).await.unwrap();

        let batches = sql_context
            .sql("SELECT id, name, value FROM paimon.test_db.t_multi_col ORDER BY id")
            .await
            .unwrap()
            .collect()
            .await
            .unwrap();

        let rows = collect_rows(&batches);
        assert_eq!(
            rows,
            vec![
                (1, "alice".to_string(), 10),
                (2, "updated".to_string(), 0),
                (3, "charlie".to_string(), 30),
            ]
        );
    }

    #[tokio::test]
    async fn test_update_no_matching_rows() {
        let (_tmp, sql_context, table) = setup_data_evolution_table("t_no_match").await;

        let update =
            parse_update("UPDATE paimon.test_db.t_no_match SET name = 'nobody' WHERE id = 99");
        let result = execute_update(&sql_context, &update, table).await.unwrap();
        let batches = result.collect().await.unwrap();
        let count = batches[0]
            .column(0)
            .as_any()
            .downcast_ref::<UInt64Array>()
            .unwrap()
            .value(0);
        assert_eq!(count, 0);
    }

    #[tokio::test]
    async fn test_update_row_id_stability() {
        let (_tmp, sql_context, table) = setup_data_evolution_table("t_row_id").await;

        // Get row IDs before update
        let before = sql_context
            .sql("SELECT id, \"_ROW_ID\" FROM paimon.test_db.t_row_id ORDER BY id")
            .await
            .unwrap()
            .collect()
            .await
            .unwrap();

        let update = parse_update("UPDATE paimon.test_db.t_row_id SET name = 'ALICE' WHERE id = 1");
        execute_update(&sql_context, &update, table).await.unwrap();

        // Get row IDs after update
        let after = sql_context
            .sql("SELECT id, \"_ROW_ID\" FROM paimon.test_db.t_row_id ORDER BY id")
            .await
            .unwrap()
            .collect()
            .await
            .unwrap();

        // Row IDs should remain the same
        assert_eq!(before, after);
    }

    #[tokio::test]
    async fn test_update_rejects_pk_table_without_data_evolution() {
        let file_io = FileIOBuilder::new("memory").build().unwrap();
        let table_path = "memory:/test_update_reject";
        file_io
            .mkdirs(&format!("{table_path}/snapshot/"))
            .await
            .unwrap();
        file_io
            .mkdirs(&format!("{table_path}/manifest/"))
            .await
            .unwrap();

        let schema = PaimonSchema::builder()
            .column("id", DataType::Int(IntType::new()))
            .primary_key(["id"])
            .option("bucket", "1")
            .build()
            .unwrap();
        let table_schema = TableSchema::new(0, &schema);
        let table = Table::new(
            file_io,
            Identifier::new("default", "t"),
            table_path.to_string(),
            table_schema,
            None,
        );

        let sql_context = SQLContext::new();
        let update = parse_update("UPDATE t SET id = 1");
        let result = execute_update(&sql_context, &update, table).await;
        assert!(result.is_err());
        assert!(result
            .unwrap_err()
            .to_string()
            .contains("primary-key tables without data-evolution"));
    }

    // -----------------------------------------------------------------------
    // CoW UPDATE tests (append-only tables)
    // -----------------------------------------------------------------------

    async fn setup_append_only_table(name: &str) -> (TempDir, SQLContext) {
        let (tmp, sql_context, _catalog) = setup_sql_context().await;

        sql_context
            .sql(&format!(
                "CREATE TABLE paimon.test_db.{name} (id INT, name VARCHAR, value INT)"
            ))
            .await
            .unwrap();

        sql_context
            .sql(&format!(
                "INSERT INTO paimon.test_db.{name} (id, name, value) VALUES (1, 'alice', 10), (2, 'bob', 20), (3, 'charlie', 30)"
            ))
            .await
            .unwrap()
            .collect()
            .await
            .unwrap();

        (tmp, sql_context)
    }

    async fn query_rows(sql_context: &SQLContext, table: &str) -> Vec<(i32, String, i32)> {
        let batches = sql_context
            .sql(&format!("SELECT id, name, value FROM {table} ORDER BY id"))
            .await
            .unwrap()
            .collect()
            .await
            .unwrap();
        collect_rows(&batches)
    }

    #[tokio::test]
    async fn test_cow_update_with_where() {
        let (_tmp, sql_context) = setup_append_only_table("t_cow_where").await;

        sql_context
            .sql("UPDATE paimon.test_db.t_cow_where SET name = 'ALICE' WHERE id = 1")
            .await
            .unwrap();

        let rows = query_rows(&sql_context, "paimon.test_db.t_cow_where").await;
        assert_eq!(
            rows,
            vec![
                (1, "ALICE".to_string(), 10),
                (2, "bob".to_string(), 20),
                (3, "charlie".to_string(), 30),
            ]
        );
    }

    #[tokio::test]
    async fn test_cow_update_without_where() {
        let (_tmp, sql_context) = setup_append_only_table("t_cow_no_where").await;

        sql_context
            .sql("UPDATE paimon.test_db.t_cow_no_where SET value = 99")
            .await
            .unwrap();

        let rows = query_rows(&sql_context, "paimon.test_db.t_cow_no_where").await;
        assert_eq!(
            rows,
            vec![
                (1, "alice".to_string(), 99),
                (2, "bob".to_string(), 99),
                (3, "charlie".to_string(), 99),
            ]
        );
    }

    #[tokio::test]
    async fn test_cow_update_multiple_columns() {
        let (_tmp, sql_context) = setup_append_only_table("t_cow_multi").await;

        sql_context
            .sql("UPDATE paimon.test_db.t_cow_multi SET name = 'updated', value = 0 WHERE id = 2")
            .await
            .unwrap();

        let rows = query_rows(&sql_context, "paimon.test_db.t_cow_multi").await;
        assert_eq!(
            rows,
            vec![
                (1, "alice".to_string(), 10),
                (2, "updated".to_string(), 0),
                (3, "charlie".to_string(), 30),
            ]
        );
    }

    #[tokio::test]
    async fn test_cow_update_no_matching_rows() {
        let (_tmp, sql_context) = setup_append_only_table("t_cow_nomatch").await;

        let result = sql_context
            .sql("UPDATE paimon.test_db.t_cow_nomatch SET name = 'nobody' WHERE id = 99")
            .await
            .unwrap()
            .collect()
            .await
            .unwrap();
        let count = result[0]
            .column(0)
            .as_any()
            .downcast_ref::<UInt64Array>()
            .unwrap()
            .value(0);
        assert_eq!(count, 0);
    }

    #[tokio::test]
    async fn test_cow_update_expression() {
        let (_tmp, sql_context) = setup_append_only_table("t_cow_expr").await;

        sql_context
            .sql("UPDATE paimon.test_db.t_cow_expr SET value = value + 100 WHERE id >= 2")
            .await
            .unwrap();

        let rows = query_rows(&sql_context, "paimon.test_db.t_cow_expr").await;
        assert_eq!(
            rows,
            vec![
                (1, "alice".to_string(), 10),
                (2, "bob".to_string(), 120),
                (3, "charlie".to_string(), 130),
            ]
        );
    }
}
