use std::collections::HashMap;

use itertools::Itertools;

use crate::diff::{diff, ChangeType, Diff, DiffOptions, DiffSql};

use super::{
    constraint::Constraint, domain::Domain, extension::Extension, index::Index, r#enum::Enum,
    routine::Routine, sequence::Sequence, table::Table, trigger::Trigger, view::View,
};

#[derive(Debug, Default, Eq, PartialEq)]
/// All diffable objects within a single PostgreSQL schema.
pub struct Schema {
    /// Schema name.
    pub name: String,
    /// Enum types keyed by name.
    pub enums: HashMap<String, Enum>,
    /// Domains keyed by name.
    pub domains: HashMap<String, Domain>,
    /// Sequences keyed by name.
    pub sequences: HashMap<String, Sequence>,
    /// Extensions keyed by name.
    pub extensions: HashMap<String, Extension>,
    /// Standalone indexes keyed by name.
    pub indexes: HashMap<String, Index>,
    /// Tables keyed by name.
    pub tables: HashMap<String, Table>,
    /// Views and materialized views keyed by name.
    pub views: HashMap<String, View>,
    /// Routines keyed by `(name, identity_arguments)`.
    pub routines: HashMap<(String, String), Routine>,
    /// Triggers keyed by `(table_name, trigger_name)`.
    pub triggers: HashMap<(String, String), Trigger>,
    /// Constraints keyed by `(table_name, constraint_name)`.
    pub constraints: HashMap<(String, String), Constraint>,
}

impl Schema {
    /// Creates an empty schema model with the given name.
    pub fn new(name: &str) -> Self {
        Self {
            name: name.to_owned(),
            ..Default::default()
        }
    }
    /// Diffs the schema tables against another schema.
    pub fn diff_tables<'a>(&'a self, other: &'a Self) -> Diff<'a, Table> {
        diff(
            self.tables.values().sorted_by(|a, b| a.name.cmp(&b.name)),
            other.tables.values().sorted_by(|a, b| a.name.cmp(&b.name)),
            |table| &table.name,
        )
    }
    /// Diffs the schema enums against another schema.
    pub fn diff_enums<'a>(&'a self, other: &'a Self) -> Diff<'a, Enum> {
        diff(
            self.enums.values().sorted_by(|a, b| a.name.cmp(&b.name)),
            other.enums.values().sorted_by(|a, b| a.name.cmp(&b.name)),
            |e| &e.name,
        )
    }
    /// Diffs the schema domains against another schema.
    pub fn diff_domains<'a>(&'a self, other: &'a Self) -> Diff<'a, Domain> {
        diff(
            self.domains.values().sorted_by(|a, b| a.name.cmp(&b.name)),
            other.domains.values().sorted_by(|a, b| a.name.cmp(&b.name)),
            |d| &d.name,
        )
    }
    /// Diffs the schema sequences against another schema.
    pub fn diff_sequences<'a>(&'a self, other: &'a Self) -> Diff<'a, Sequence> {
        diff(
            self.sequences
                .values()
                .sorted_by(|a, b| a.name.cmp(&b.name)),
            other
                .sequences
                .values()
                .sorted_by(|a, b| a.name.cmp(&b.name)),
            |s| &s.name,
        )
    }
    /// Diffs the schema extensions against another schema.
    pub fn diff_extensions<'a>(&'a self, other: &'a Self) -> Diff<'a, Extension> {
        diff(
            self.extensions
                .values()
                .sorted_by(|a, b| a.name.cmp(&b.name)),
            other
                .extensions
                .values()
                .sorted_by(|a, b| a.name.cmp(&b.name)),
            |e| &e.name,
        )
    }
    /// Diffs the schema indexes against another schema.
    pub fn diff_indexes<'a>(&'a self, other: &'a Self) -> Diff<'a, Index> {
        diff(
            self.indexes.values().sorted_by(|a, b| a.name.cmp(&b.name)),
            other.indexes.values().sorted_by(|a, b| a.name.cmp(&b.name)),
            |index| &index.name,
        )
    }
    /// Diffs the schema constraints against another schema.
    pub fn diff_constraints<'a>(&'a self, other: &'a Self) -> Diff<'a, Constraint> {
        diff(
            self.constraints
                .values()
                .sorted_by(|a, b| (&a.table, &a.name).cmp(&(&b.table, &b.name))),
            other
                .constraints
                .values()
                .sorted_by(|a, b| (&a.table, &a.name).cmp(&(&b.table, &b.name))),
            |c| (&c.table, &c.name),
        )
    }
    /// Diffs the schema routines against another schema.
    pub fn diff_routines<'a>(&'a self, other: &'a Self) -> Diff<'a, Routine> {
        diff(
            self.routines.values().sorted_by(|a, b| {
                (&a.name, &a.identity_arguments).cmp(&(&b.name, &b.identity_arguments))
            }),
            other.routines.values().sorted_by(|a, b| {
                (&a.name, &a.identity_arguments).cmp(&(&b.name, &b.identity_arguments))
            }),
            |f| (&f.name, &f.identity_arguments),
        )
    }
    /// Diffs the schema triggers against another schema.
    pub fn diff_triggers<'a>(&'a self, other: &'a Self) -> Diff<'a, Trigger> {
        diff(
            self.triggers
                .values()
                .sorted_by(|a, b| (&a.table_name, &a.name).cmp(&(&b.table_name, &b.name))),
            other
                .triggers
                .values()
                .sorted_by(|a, b| (&a.table_name, &a.name).cmp(&(&b.table_name, &b.name))),
            |t| (&t.table_name, &t.name),
        )
    }
}

impl DiffSql for Diff<'_, Schema> {
    fn sql(&self, opts: &DiffOptions) -> Vec<(ChangeType, String)> {
        let mut v = Vec::new();
        if !self.a_only.is_empty() {
            todo!("Schema creation not supported, yet.")
        }
        for (a, b) in &self.a_and_b {
            v.extend(a.diff_triggers(b).sql(opts));
            v.extend(a.diff_enums(b).sql(opts));
            v.extend(a.diff_domains(b).sql(opts));
            v.extend(a.diff_sequences(b).sql(opts));
            v.extend(a.diff_extensions(b).sql(opts));
            v.extend(a.diff_routines(b).sql(opts));
            v.extend(a.diff_tables(b).sql(opts));
            v.extend(a.diff_indexes(b).sql(opts));
            v.extend(a.diff_constraints(b).sql(opts));
        }
        if !self.b_only.is_empty() {
            println!("{:?}", self.b_only);
            todo!("Schema creation not supported, yet.")
        }
        v
    }
}

/// Joins ordered SQL fragments into a single migration script.
pub fn join_sql(v: Vec<(ChangeType, String)>) -> String {
    v.into_iter()
        .sorted_by(|a, b| a.0.cmp(&b.0))
        .map(|t| t.1)
        .join("\n")
}
