package backup

/*
 * This file contains structs and functions related to executing specific
 * queries to gather metadata for the objects handled in predata_general.go.
 */

import (
	"fmt"

	"github.com/apache/cloudberry-backup/toc"
	"github.com/apache/cloudberry-backup/utils"
	"github.com/apache/cloudberry-go-libs/dbconn"
	"github.com/apache/cloudberry-go-libs/gplog"
)

type Operator struct {
	Oid              uint32
	Schema           string
	Name             string
	Procedure        string
	LeftArgType      string
	RightArgType     string
	CommutatorOp     string
	NegatorOp        string
	RestrictFunction string
	JoinFunction     string
	CanHash          bool
	CanMerge         bool
}

func (o Operator) GetMetadataEntry() (string, toc.MetadataEntry) {
	return "predata",
		toc.MetadataEntry{
			Schema:          o.Schema,
			Name:            o.Name,
			ObjectType:      toc.OBJ_OPERATOR,
			ReferenceObject: "",
			StartByte:       0,
			EndByte:         0,
		}
}

func (o Operator) GetUniqueID() UniqueID {
	return UniqueID{ClassID: PG_OPERATOR_OID, Oid: o.Oid}
}

func (o Operator) FQN() string {
	leftArg := "NONE"
	rightArg := "NONE"
	if o.LeftArgType != "-" {
		leftArg = o.LeftArgType
	}
	if o.RightArgType != "-" {
		rightArg = o.RightArgType
	}
	return fmt.Sprintf("%s.%s (%s, %s)", o.Schema, o.Name, leftArg, rightArg)
}

func GetOperators(connectionPool *dbconn.DBConn) []Operator {
	results := make([]Operator, 0)
	query := fmt.Sprintf(`
	SELECT o.oid AS oid,
		quote_ident(n.nspname) AS schema,
		oprname AS name,
		oprcode::regproc AS procedure,
		oprleft::regtype AS leftargtype,
		oprright::regtype AS rightargtype,
		oprcom::regoper AS commutatorop,
		oprnegate::regoper AS negatorop,
		oprrest AS restrictfunction,
		oprjoin AS joinfunction,
		oprcanmerge AS canmerge,
		oprcanhash AS canhash
	FROM pg_operator o
		JOIN pg_namespace n on n.oid = o.oprnamespace
	WHERE %s AND oprcode != 0
		AND %s`, SchemaFilterClause("n"), ExtensionFilterClause("o"))

	err := connectionPool.Select(&results, query)
	gplog.FatalOnError(err)
	return results
}

type OperatorFamily struct {
	Oid         uint32
	Schema      string
	Name        string
	IndexMethod string
}

func (opf OperatorFamily) GetMetadataEntry() (string, toc.MetadataEntry) {
	return "predata",
		toc.MetadataEntry{
			Schema:          opf.Schema,
			Name:            opf.Name,
			ObjectType:      toc.OBJ_OPERATOR_FAMILY,
			ReferenceObject: "",
			StartByte:       0,
			EndByte:         0,
		}
}

func (opf OperatorFamily) GetUniqueID() UniqueID {
	return UniqueID{ClassID: PG_OPFAMILY_OID, Oid: opf.Oid}
}

func (opf OperatorFamily) FQN() string {
	return fmt.Sprintf("%s USING %s", utils.MakeFQN(opf.Schema, opf.Name), opf.IndexMethod)
}

func GetOperatorFamilies(connectionPool *dbconn.DBConn) []OperatorFamily {
	results := make([]OperatorFamily, 0)
	query := fmt.Sprintf(`
	SELECT o.oid AS oid,
		quote_ident(n.nspname) AS schema,
		quote_ident(opfname) AS name,
		(SELECT quote_ident(amname) FROM pg_am WHERE oid = opfmethod) AS indexMethod
	FROM pg_opfamily o
		JOIN pg_namespace n on n.oid = o.opfnamespace
	WHERE %s
		AND %s`,
		SchemaFilterClause("n"), ExtensionFilterClause("o"))
	err := connectionPool.Select(&results, query)
	gplog.FatalOnError(err)
	return results
}

type OperatorClass struct {
	Oid          uint32
	Schema       string
	Name         string
	FamilySchema string
	FamilyName   string
	IndexMethod  string
	Type         string
	Default      bool
	StorageType  string
	Operators    []OperatorClassOperator
	Functions    []OperatorClassFunction
}

func (opc OperatorClass) GetMetadataEntry() (string, toc.MetadataEntry) {
	return "predata",
		toc.MetadataEntry{
			Schema:          opc.Schema,
			Name:            opc.Name,
			ObjectType:      toc.OBJ_OPERATOR_CLASS,
			ReferenceObject: "",
			StartByte:       0,
			EndByte:         0,
		}
}

func (opc OperatorClass) GetUniqueID() UniqueID {
	return UniqueID{ClassID: PG_OPCLASS_OID, Oid: opc.Oid}
}

func (opc OperatorClass) FQN() string {
	return fmt.Sprintf("%s USING %s", utils.MakeFQN(opc.Schema, opc.Name), opc.IndexMethod)
}

func GetOperatorClasses(connectionPool *dbconn.DBConn) []OperatorClass {
	results := make([]OperatorClass, 0)
	query := fmt.Sprintf(`
	SELECT c.oid AS oid,
		quote_ident(cls_ns.nspname) AS schema,
		quote_ident(opcname) AS name,
		quote_ident(fam_ns.nspname) AS familyschema,
		quote_ident(opfname) AS familyname,
		(SELECT amname FROM pg_catalog.pg_am WHERE oid = opcmethod) AS indexmethod,
		opcintype::pg_catalog.regtype AS type,
		opcdefault AS default,
		/*
		 * GPDB7 (PG12) and CBDB (PG14) have different behaviors when casting
		 * opckeytype to a regtype when the STORAGE type is the same as the
		 * FOR TYPE and the opckeytype is not 0.
		 *
		 * In GPDB7, the expression opckeytype::regtype evaluates to "-"
		 * in this case.
		 *
		 * In CBDB, the same expression evaluates to the type name (e.g., "integer").
		 *
		 * This CASE statement explicitly checks if the types are the same to
		 * normalize this behavior, ensuring the output is always "-" when the
		 * storage type is the same as the input type.
		 */
		CASE WHEN opcintype = opckeytype THEN '-' ELSE opckeytype::pg_catalog.regtype END AS storagetype
	FROM pg_catalog.pg_opclass c
		LEFT JOIN pg_catalog.pg_opfamily f ON f.oid = opcfamily
		JOIN pg_catalog.pg_namespace cls_ns ON cls_ns.oid = opcnamespace
		JOIN pg_catalog.pg_namespace fam_ns ON fam_ns.oid = opfnamespace
	WHERE %s
		AND %s`,
		SchemaFilterClause("cls_ns"), ExtensionFilterClause("c"))

	err := connectionPool.Select(&results, query)
	gplog.FatalOnError(err)

	operators := GetOperatorClassOperators(connectionPool)
	for i := range results {
		results[i].Operators = operators[results[i].Oid]
	}
	functions := GetOperatorClassFunctions(connectionPool)
	for i := range results {
		results[i].Functions = functions[results[i].Oid]
	}

	return results
}

type OperatorClassOperator struct {
	ClassOid       uint32
	StrategyNumber int
	Operator       string
	Recheck        bool
	OrderByFamily  string
}

func GetOperatorClassOperators(connectionPool *dbconn.DBConn) map[uint32][]OperatorClassOperator {
	results := make([]OperatorClassOperator, 0)
	version5Query := `
	SELECT refobjid AS classoid,
		amopstrategy AS strategynumber,
		amopopr::pg_catalog.regoperator AS operator,
		amopreqcheck AS recheck
	FROM pg_catalog.pg_amop ao
		JOIN pg_catalog.pg_depend d ON d.objid = ao.oid
	WHERE refclassid = 'pg_catalog.pg_opclass'::pg_catalog.regclass
		AND classid = 'pg_catalog.pg_amop'::pg_catalog.regclass
	ORDER BY amopstrategy`

	atLeast6Query := `
	SELECT refobjid AS classoid,
		amopstrategy AS strategynumber,
		amopopr::pg_catalog.regoperator AS operator,
		coalesce(quote_ident(ns.nspname) || '.' || quote_ident(opf.opfname), '') AS orderbyfamily
	FROM pg_catalog.pg_amop ao
		JOIN pg_catalog.pg_depend d ON d.objid = ao.oid
		LEFT JOIN pg_opfamily opf ON opf.oid = ao.amopsortfamily
		LEFT JOIN pg_namespace ns ON ns.oid = opf.opfnamespace
	WHERE refclassid = 'pg_catalog.pg_opclass'::pg_catalog.regclass
		AND classid = 'pg_catalog.pg_amop'::pg_catalog.regclass
	ORDER BY amopstrategy`

	cloudberryQuery := `
	SELECT
		opc.oid AS classoid,
		amop.amopstrategy AS strategynumber,
		amop.amopopr::pg_catalog.regoperator AS operator,
		coalesce(quote_ident(sort_ns.nspname) || '.' || quote_ident(sort_opf.opfname), '') AS orderbyfamily
	FROM
		pg_catalog.pg_opclass AS opc
	JOIN
		pg_catalog.pg_amop AS amop ON opc.opcfamily = amop.amopfamily AND amop.amoplefttype = opc.opcintype
	LEFT JOIN
		pg_catalog.pg_opfamily AS sort_opf ON sort_opf.oid = amop.amopsortfamily
	LEFT JOIN
		pg_catalog.pg_namespace AS sort_ns ON sort_ns.oid = sort_opf.opfnamespace
	ORDER BY
		classoid, strategynumber`

	query := ""
	if connectionPool.Version.IsCBDB() {
		query = cloudberryQuery
	} else if connectionPool.Version.IsGPDB() && connectionPool.Version.Is("5") {
		query = version5Query
	} else {
		query = atLeast6Query
	}

	err := connectionPool.Select(&results, query)
	gplog.FatalOnError(err)

	operators := make(map[uint32][]OperatorClassOperator)
	for _, result := range results {
		operators[result.ClassOid] = append(operators[result.ClassOid], result)
	}
	return operators
}

type OperatorClassFunction struct {
	ClassOid      uint32
	SupportNumber int
	FunctionName  string
	LeftType      string `db:"amproclefttype"`
	RightType     string `db:"amprocrighttype"`
}

func GetOperatorClassFunctions(connectionPool *dbconn.DBConn) map[uint32][]OperatorClassFunction {
	results := make([]OperatorClassFunction, 0)
	query := `
	SELECT refobjid AS classoid,
		amprocnum AS supportnumber,
		amproclefttype::regtype,
		amprocrighttype::regtype,
		amproc::regprocedure::text AS functionname
	FROM pg_catalog.pg_amproc ap
		JOIN pg_catalog.pg_depend d ON d.objid = ap.oid
	WHERE refclassid = 'pg_catalog.pg_opclass'::pg_catalog.regclass
		AND classid = 'pg_catalog.pg_amproc'::pg_catalog.regclass
	ORDER BY amprocnum`

	err := connectionPool.Select(&results, query)
	gplog.FatalOnError(err)

	functions := make(map[uint32][]OperatorClassFunction)
	for _, result := range results {
		functions[result.ClassOid] = append(functions[result.ClassOid], result)
	}
	return functions
}
