use itertools::Itertools;
use thiserror::Error;

use crate::{
    diff::{diff, ChangeType, Diff, DiffOptions, DiffSql},
    queries::{Class, Relkind},
    sql::quote_ident,
};

use super::column::Column;

#[derive(Debug, Eq, PartialEq)]
/// A PostgreSQL table definition.
pub struct Table {
    /// Schema that owns the table.
    pub schema: String,
    /// Table name.
    pub name: String,
    kind: Relkind,
    /// Columns in declaration order.
    pub columns: Vec<Column>,
}

impl TryFrom<Class> for Table {
    type Error = InvalidRelkind;
    fn try_from(cls: Class) -> Result<Self, Self::Error> {
        Ok(Self {
            schema: cls.schema,
            name: cls.name,
            kind: cls.relkind,
            columns: cls.columns.0,
        })
    }
}

/// Error returned when a non-table relation is converted into a [`Table`].
#[derive(Copy, Clone, Debug, Error)]
#[error("Unsupported table for table: {0}")]
pub struct InvalidRelkind(Relkind);

impl Table {
    /// Renders a `CREATE TABLE` statement for the table.
    pub fn create(&self) -> String {
        let cols = self.columns.iter().map(|col| col.sql()).join(",\n    ");
        format!(
            "CREATE TABLE {}.{} (\n    {}\n);\n",
            quote_ident(&self.schema),
            quote_ident(&self.name),
            cols
        )
    }
    /// Renders a `DROP TABLE` statement for the table.
    pub fn drop(&self) -> String {
        format!(
            "DROP TABLE {}.{};\n",
            quote_ident(&self.schema),
            quote_ident(&self.name),
        )
    }
    /// Wraps column-level changes into a single table alteration script.
    pub fn alter_sql(&self, col_sql: Vec<(ChangeType, String)>) -> String {
        let mut output = Vec::new();
        let (errors, alter_clauses): (Vec<_>, Vec<_>) = col_sql
            .into_iter()
            .partition(|(change_type, _)| *change_type == ChangeType::Unsupported);
        let has_errors = !errors.is_empty();

        if !alter_clauses.is_empty() {
            output.push(format!(
                "ALTER TABLE {}.{}\n{};\n",
                quote_ident(&self.schema),
                quote_ident(&self.name),
                alter_clauses
                    .iter()
                    .map(|(_, sql)| format!("    {}", sql))
                    .join(",\n"),
            ));
        }

        output.extend(errors.into_iter().map(|(_, sql)| sql));

        if has_errors {
            output.push(format!(
                "DO $$\n\
BEGIN\n\
    RAISE EXCEPTION 'Unsupported schema change for table {}.{}; check the warnings above';\n\
END\n\
$$;\n",
                self.schema, self.name
            ));
        }

        output.join("\n")
    }
    /// Diffs the table columns against another table definition.
    pub fn diff_columns<'a>(&'a self, other: &'a Self) -> Diff<'a, Column> {
        diff(self.columns.iter(), other.columns.iter(), |c| &c.name)
    }
}

impl DiffSql for Diff<'_, Table> {
    fn sql(&self, opts: &DiffOptions) -> Vec<(ChangeType, String)> {
        let mut v = Vec::new();
        for a in &self.a_only {
            v.push((ChangeType::DropTable, a.drop()));
        }
        for (a, b) in &self.a_and_b {
            let cols = a.diff_columns(b);
            let col_sql = cols.sql(opts);
            if !col_sql.is_empty() {
                v.push((ChangeType::AlterColumn, b.alter_sql(col_sql)));
            }
            // Reconcile NOT NULL constraint names that drifted between the two
            // sides (PostgreSQL 18+) -- e.g. a column that was renamed keeps the
            // constraint name derived from its former name. Emitted as a
            // standalone statement because RENAME CONSTRAINT cannot be combined
            // with other ALTER TABLE actions.
            for (col_a, col_b) in &cols.a_and_b {
                if let (Some(from), Some(to)) = (&col_a.notnull_name, &col_b.notnull_name) {
                    if from != to {
                        v.push((
                            ChangeType::AlterColumn,
                            format!(
                                "ALTER TABLE {}.{} RENAME CONSTRAINT {} TO {};\n",
                                quote_ident(&b.schema),
                                quote_ident(&b.name),
                                quote_ident(from),
                                quote_ident(to),
                            ),
                        ));
                    }
                }
            }
        }
        for b in &self.b_only {
            v.push((ChangeType::CreateTable, b.create()));
        }
        v
    }
}
