#include "PgClass.h"
#include "PgAttribute.h"
#include "PgAttributeContainer.h"
#include "PgClassContainer.h"
#include "PgDatabaseCatalog.h"
#include "PgConstraintContainer.h"
#include "PgInheritsContainer.h"
#include <QStringBuilder>
#include "SqlFormattingUtils.h"
#include <ranges>
#include <cassert>


void operator<<(RelPersistence &s, const Pgsql::Value &v)
{
	//s = static_cast<T>(v);
	const char *c = v.c_str();
    switch (*c)
    {
	case 'p':
		s = RelPersistence::Permanent;
		break;
	case 'u':
		s = RelPersistence::Unlogged;
		break;
	case 't':
		s = RelPersistence::Temporary;
		break;
	}
}

void operator<<(RelKind &s, const Pgsql::Value &v)
{
	//s = static_cast<T>(v);
	const char *c = v.c_str();
    switch (*c)
    {
        case 'r':
            s = RelKind::Table;
            break;
        case 'i':
            s = RelKind::Index;
            break;
        case 'S':
            s = RelKind::Sequence;
            break;
        case 'v':
            s = RelKind::View;
            break;
        case 'm':
            s = RelKind::MaterializedView;
            break;
        case 'c':
            s = RelKind::Composite;
            break;
        case 't':
            s = RelKind::Toast;
            break;
        case 'f':
            s = RelKind::ForeignTable;
            break;
        case 'p':
            s = RelKind::PartitionedTable;
            break;
        case 'I':
            s = RelKind::PartitionedIndex;
            break;
        default:
            throw std::runtime_error("Unknown RelKind");
	}
}

void operator<<(PartitioningStrategy &s, const Pgsql::Value &v)
{
    const char *c = v.c_str();
    switch (*c)
    {
        case 'h':
            s = PartitioningStrategy::Hash;
            break;
        case 'l':
            s = PartitioningStrategy::List;
            break;
        case 'r':
            s = PartitioningStrategy::Range;
            break;
        default:
            throw std::runtime_error("Unknown PartitioningStrategy");
    }
}

QString PgClass::createSql() const
{
    if (createSqlCache.isEmpty())
    {
        switch (kind)
        {
            case RelKind::Table:
            case RelKind::PartitionedTable:
                createSqlCache = createTableSql();
                break;
            case RelKind::View:
                createSqlCache = createViewSql();
                break;
        }
	}
	return createSqlCache;
}

QString PgClass::typeName() const
{
    switch (kind)
    {
        case RelKind::Table: return "TABLE";
        case RelKind::Index: return "INDEX";
        case RelKind::Sequence: return "SEQUENCE";
        case RelKind::View: return "VIEW";
        case RelKind::MaterializedView: return "MATERIALIZED VIEW";
        case RelKind::Composite: return "COMPOSITE";
        case RelKind::Toast: return "TOAST";
        case RelKind::ForeignTable: return "FOREIGN TABLE";
        case RelKind::PartitionedTable: return "PARTITIONED TABLE";
        case RelKind::PartitionedIndex: return "PARTITIONED INDEX";
	}
	throw std::runtime_error("Unexpected value in PgClass::typeName()");
}

QString PgClass::aclAllPattern() const
{
    switch (kind)
    {
	case RelKind::Table: return "arwdDxt";
	default:
		break;
	}
    return {};
}

QString PgClass::ddlTypeName() const
{
    switch (kind)
    {
        case RelKind::PartitionedTable:
            return "TABLE";
        default:
            return PgNamespaceObject::ddlTypeName();
    }
}

QString PgClass::createTableSql() const
{
	QString sql;
	//			CREATE [ TEMP | UNLOGGED ] TABLE [ IF NOT EXISTS ] table_name ( [
	sql += "CREATE ";
	if (persistence == RelPersistence::Unlogged)
		sql += "UNLOGGED ";
	else if (persistence == RelPersistence::Temporary)
		sql += "TEMP ";
	sql += "TABLE ";
	sql += fullyQualifiedQuotedObjectName();

    if (!partitionBoundaries.isEmpty())
    {
        sql += " PARTITION OF " + getPartitionOfName();
        sql += generateBodySql(true);
    }
    else
        sql += generateBodySql(false);


    sql += generateInheritsSql()
        % partitionBySql()
        % generateTablespaceSql()
        % ";\n";
    return sql;
}

QString PgClass::generateBodySql(bool isPartition) const
{
    // - also remove commented inherited column list? They are listed in the column view no need for them in sql
    // - mark them in the view as inherited?
    // - detect when body empty and leave it out completely (only for partitions, is leaving it out always legal?)
    // - need to detect "inherited" constraint because these should not be listed either


    auto colsFilter = isPartition ?
            [] (const PgAttribute &c)
                    {
                        return c.num > 0 // ignore system columns
                            && !c.isdropped // ignore dropped columns
                            && c.islocal;
                    }
            :
            [] (const PgAttribute &c)
                    {
                        return c.num > 0 // ignore system columns
                            && !c.isdropped; // ignore dropped columns
                    }
            ;

    auto && cols = catalog().attributes()->getColumnsForRelation(oid())
        | std::views::filter(colsFilter);
    auto && constraints = catalog().constraints()->getConstraintsForRelation(oid())
        | std::views::filter([] (const auto &c) { return c.islocal; });

    if (cols.empty() && constraints.empty())
        return {};

    QString sql = " (\n    ";

    bool first = true;
    for (auto && col : cols)
    {
        if (first)
        {
            first = false;
        }
        else
            sql += ",\n    ";
        if (!col.islocal) sql += "-- ";
        sql += col.columnDefinition(catalog());
    }


    for (auto && constraint: constraints)
    {
        if (first)
        {
            sql += "\n    ";
            first = false;
        }
        else
            sql += ",\n    ";
        sql += getConstraintDefinition(catalog(), constraint, "      ");
    }

    sql += "\n)";

    return sql;
}

QString PgClass::generateInheritsSql() const
{
    if (!partitionBoundaries.isEmpty())
        return "\n" + partitionBoundaries;

    QString sql;
    //			[ INHERITS ( parent_table [, ... ] ) ]
    auto parents = catalog().inherits()->getParentsOf(oid());
    if (!parents.empty())
    {
        sql += "\nINHERITS (";
        bool first = true;
        for (auto parent_oid : parents)
        {
            if (first) first = false;
            else sql += ", ";
            sql += catalog().classes()->getByKey(parent_oid)->fullyQualifiedQuotedObjectName();
        }
        sql += ")";
    }
    return sql;
}

QString PgClass::partitionBySql() const
{
    if (kind != RelKind::PartitionedTable)
        return {};

    QString sql = "\nPARTITION BY " % PartitionStrategyKeyword(partitionedTable.strategy);
    sql += partitionKeySql();
    return sql;
}

QString PgClass::partitionKeySql() const
{
    QString result;
    result += "(";
    auto keyItem = partitionedTable.keyColumns.begin();
    if (keyItem != partitionedTable.keyColumns.end())
    {
        result += partitionKeyItemSql(*keyItem);
        for (++keyItem; keyItem != partitionedTable.keyColumns.end(); ++keyItem)
            result += ", " % partitionKeyItemSql(*keyItem);
    }
    result += ")";
    return result;
}

QString PgClass::partitionKeyItemSql(
    const PartitioningKeyItem &keyItem
) const
{
    if (keyItem.attNum == 0)
        return keyItem.expression;

    const PgAttribute *col = catalog().attributes()->findIf(
        [this, &keyItem] (const auto &att)
        {
            return att.relid == oid() && att.num == keyItem.attNum;
        }
    );

    assert(col != nullptr);
    return quoteIdent(col->name);
}

QString PgClass::getPartitionOfName() const
{
    auto parents = catalog().inherits()->getParentsOf(oid());
    if (!parents.empty())
    {
        return catalog().classes()->getByKey(parents.front())->fullyQualifiedQuotedObjectName();
    }
    throw std::logic_error("Should only be called if there is a parent table");
}

QString PgClass::generateTablespaceSql() const
{
    if (tablespace != 0)
    {
        auto ns = getTablespaceDisplayString(catalog(), tablespace);
        return "\n  TABLESPACE " % quoteIdent(ns);
    }
    return {};
}

QString PgClass::createViewSql() const
{
	QString sql;
	sql += "CREATE OR REPLACE VIEW " + fullyQualifiedQuotedObjectName();

	// todo security_barrier
	// todo check_option

	sql += " AS \n";
	sql += viewdef;

	// todo owner

	return sql;
}

QString PartitionStrategyKeyword(PartitioningStrategy ps)
{
    switch (ps) {
        case PartitioningStrategy::Hash:
            return "HASH";
        case PartitioningStrategy::List:
            return "LIST";
        case PartitioningStrategy::Range:
            return "RANGE";
    }
    throw std::runtime_error("Unknown PartitioningStrategy");
}
