package backup

/*
 * This file contains structs and functions related to backing up metadata on the
 * coordinator for objects that connect to external data (external tables and external
 * protocols).
 */

import (
	"database/sql"
	"fmt"
	"strings"

	"github.com/apache/cloudberry-backup/toc"
	"github.com/apache/cloudberry-backup/utils"
)

const (
	// Type of external table
	READABLE = iota
	READABLE_WEB
	WRITABLE
	WRITABLE_WEB
	// Protocol external table is using
	FILE
	GPFDIST
	GPHDFS
	HTTP
	S3
)

type ExternalTableDefinition struct {
	Oid             uint32
	Type            int
	Protocol        int
	Location        sql.NullString
	ExecLocation    string
	FormatType      string
	FormatOpts      string
	Command         string
	RejectLimit     int
	RejectLimitType string
	ErrTableName    string
	ErrTableSchema  string
	LogErrors       bool
	LogErrPersist   bool
	Encoding        string
	Writable        bool
	URIs            []string
}

func PrintExternalTableCreateStatement(metadataFile *utils.FileWithByteCount, objToc *toc.TOC, table Table) {
	start := metadataFile.ByteCount
	tableTypeStrMap := map[int]string{
		READABLE:     "READABLE EXTERNAL",
		READABLE_WEB: "READABLE EXTERNAL WEB",
		WRITABLE:     "WRITABLE EXTERNAL",
		WRITABLE_WEB: "WRITABLE EXTERNAL WEB",
	}
	extTableDef := table.ExtTableDef
	extTableDef.Type, extTableDef.Protocol = DetermineExternalTableCharacteristics(extTableDef)
	metadataFile.MustPrintf("\n\nCREATE %s TABLE %s (\n", tableTypeStrMap[extTableDef.Type], table.FQN())
	printColumnDefinitions(metadataFile, table.ColumnDefs, "")
	metadataFile.MustPrintf(") ")
	PrintExternalTableStatements(metadataFile, table.FQN(), extTableDef)
	if extTableDef.Writable {
		metadataFile.MustPrintf("\n%s", table.DistPolicy.Policy)
	}
	metadataFile.MustPrintf(";")
	if objToc != nil {
		section, entry := table.GetMetadataEntry()
		tier := globalTierMap[table.GetUniqueID()]
		objToc.AddMetadataEntry(section, entry, start, metadataFile.ByteCount, tier)
	}
}

func DetermineExternalTableCharacteristics(extTableDef ExternalTableDefinition) (int, int) {
	isWritable := extTableDef.Writable
	var tableType int
	tableProtocol := -1
	if !extTableDef.Location.Valid ||
		extTableDef.Location.String == "" { // EXTERNAL WEB tables may have EXECUTE instead of LOCATION
		tableProtocol = HTTP
		if isWritable {
			tableType = WRITABLE_WEB
		} else {
			tableType = READABLE_WEB
		}
	} else {
		/*
		 * All data sources must use the same protocol, so we can use Location to determine
		 * the table's protocol even though it only holds one data source URI.
		 */
		isWeb := strings.HasPrefix(extTableDef.Location.String, "http")
		if isWeb && isWritable {
			tableType = WRITABLE_WEB
		} else if isWeb && !isWritable {
			tableType = READABLE_WEB
		} else if !isWeb && isWritable {
			tableType = WRITABLE
		} else {
			tableType = READABLE
		}
		prefix := extTableDef.Location.String[0:strings.Index(extTableDef.Location.String, "://")]
		switch prefix {
		case "file":
			tableProtocol = FILE
		case "gpfdist":
			tableProtocol = GPFDIST
		case "gpfdists":
			tableProtocol = GPFDIST
		case "gphdfs":
			tableProtocol = GPHDFS
		case "http":
			tableProtocol = HTTP
		case "https":
			tableProtocol = HTTP
		case "s3":
			tableProtocol = S3
		}
	}
	return tableType, tableProtocol
}

func generateExecuteStatement(extTableDef ExternalTableDefinition) string {
	var executeStatement string

	extTableDef.Command = strings.Replace(extTableDef.Command, `'`, `''`, -1)
	executeStatement += fmt.Sprintf("EXECUTE '%s'", extTableDef.Command)
	execType := strings.Split(extTableDef.ExecLocation, ":")
	switch execType[0] {
	case "ALL_SEGMENTS": // Default case, don't print anything else
	case "HOST":
		executeStatement += fmt.Sprintf(" ON HOST '%s'", execType[1])
	case "MASTER_ONLY":
		executeStatement += " ON MASTER"
	case "COORDINATOR_ONLY":
		executeStatement += " ON COORDINATOR"
	case "PER_HOST":
		executeStatement += " ON HOST"
	case "SEGMENT_ID":
		executeStatement += fmt.Sprintf(" ON SEGMENT %s", execType[1])
	case "TOTAL_SEGS":
		executeStatement += fmt.Sprintf(" ON %s", execType[1])
	}

	return executeStatement
}

/*
 * This function is adapted from dumputils.c
 *
 * Escape backslashes and apostrophes in EXTERNAL TABLE format strings.
 * Returns a list of unquoted keyword and escaped quoted string tokens
 *
 * The fmtopts field of a pg_exttable tuple has an odd encoding -- it is
 * partially parsed and contains "string" values that aren't legal SQL.
 * Each string value is delimited by apostrophes and is usually, but not
 * always, a single character.	The fmtopts field is typically something
 * like {delimiter '\x09' null '\N' escape '\'} or
 * {delimiter ',' null '' escape '\' quote '''}.  Each backslash and
 * apostrophe in a string must be escaped and each string must be
 * prepended with an 'E' denoting an "escape syntax" string.
 *
 * Usage note: A field value containing an apostrophe followed by a space
 * will throw this algorithm off -- it presumes no embedded spaces.
 */
func tokenizeAndEscapeFormatOpts(formatOpts string) []string {
	inString := false
	resultList := make([]string, 0)
	currString := ""

	for i := 0; i < len(formatOpts); i++ {
		switch formatOpts[i] {
		case '\'':
			if inString {
				/*
				 * Escape apostrophes *within* the string.	If the
				 * apostrophe is at the end of the source string or is
				 * followed by a space, it is presumed to be a closing
				 * apostrophe and is not escaped.
				 */
				if (i+1) == len(formatOpts) || formatOpts[i+1] == ' ' {
					inString = false
				} else {
					currString += "\\"
				}
			} else {
				currString = "E"
				inString = true
			}
		case '\\':
			currString += "\\"
		case ' ':
			if !inString {
				resultList = append(resultList, currString)
				currString = ""
				continue
			}
		}
		currString += string(formatOpts[i])
	}
	resultList = append(resultList, currString)

	return resultList
}

/*
 * Format options to use `a = b` format because this format is required
 * when using CUSTOM format.
 *
 * We do this for CUSTOM, AVRO and PARQUET, but not CSV or TEXT because
 * CSV and TEXT have some multi-word options that are difficult
 * to parse into this format
 */
func makeCustomFormatOpts(tokens []string) string {
	var key string
	var value string
	resultOpts := make([]string, 0)

	for i := 0; i < len(tokens)-1; i += 2 {
		key = tokens[i]
		value = tokens[i+1]
		opt := fmt.Sprintf(`%s = %s`, key, value)
		resultOpts = append(resultOpts, opt)
	}
	return strings.Join(resultOpts, ", ")
}

func GenerateFormatStatement(extTableDef ExternalTableDefinition) string {
	var formatStatement string
	formatType := ""
	switch extTableDef.FormatType {
	case "t":
		formatType = "TEXT"
	case "c":
		formatType = "CSV"
	case "b":
		formatType = "CUSTOM"
	case "a":
		formatType = "AVRO"
	case "p":
		formatType = "PARQUET"
	}
	formatStatement += fmt.Sprintf("FORMAT '%s'", formatType)

	if extTableDef.FormatOpts != "" {
		formatTokens := tokenizeAndEscapeFormatOpts(strings.TrimSpace(extTableDef.FormatOpts))
		formatOptsString := ""
		if formatType == "TEXT" || formatType == "CSV" {
			formatOptsString = strings.Join(formatTokens, " ")
		} else {
			formatOptsString = makeCustomFormatOpts(formatTokens)
		}
		formatStatement += fmt.Sprintf(" (%s)", formatOptsString)
	}

	return formatStatement
}

func generateLogErrorStatement(extTableDef ExternalTableDefinition) string {
	logErrorStatement := ""
	if extTableDef.LogErrors {
		if extTableDef.LogErrPersist {
			logErrorStatement += "\nLOG ERRORS PERSISTENTLY"
		} else {
			logErrorStatement += "\nLOG ERRORS"
		}
	} else if extTableDef.ErrTableName != "" && extTableDef.ErrTableSchema != "" {
		errTableFQN := utils.MakeFQN(extTableDef.ErrTableSchema, extTableDef.ErrTableName)
		logErrorStatement += fmt.Sprintf("\nLOG ERRORS INTO %s", errTableFQN)
	}
	if extTableDef.RejectLimit != 0 {
		logErrorStatement += fmt.Sprintf("\nSEGMENT REJECT LIMIT %d ", extTableDef.RejectLimit)
		switch extTableDef.RejectLimitType {
		case "r":
			logErrorStatement += "ROWS"
		case "p":
			logErrorStatement += "PERCENT"
		}
	}

	return logErrorStatement
}

func PrintExternalTableStatements(metadataFile *utils.FileWithByteCount, tableName string, extTableDef ExternalTableDefinition) {
	if extTableDef.Type != WRITABLE_WEB {
		if len(extTableDef.URIs) > 0 {
			metadataFile.MustPrintf("LOCATION (\n\t'%s'\n)", strings.Join(extTableDef.URIs, "',\n\t'"))
		}
	}
	if extTableDef.Type == READABLE || (extTableDef.Type == WRITABLE_WEB && extTableDef.Protocol == S3) {
		if extTableDef.ExecLocation == "COORDINATOR_ONLY" {
			metadataFile.MustPrintf(" ON COORDINATOR")
		} else if extTableDef.ExecLocation == "COORDINATOR_ONLY" {
			metadataFile.MustPrintf(" ON COORDINATOR")
		}
	}
	if extTableDef.Type == READABLE_WEB || extTableDef.Type == WRITABLE_WEB {
		if extTableDef.Command != "" {
			metadataFile.MustPrint(generateExecuteStatement(extTableDef))
		}
	}
	metadataFile.MustPrintln()
	metadataFile.MustPrint(GenerateFormatStatement(extTableDef))
	metadataFile.MustPrintln()
	metadataFile.MustPrintf("ENCODING '%s'", extTableDef.Encoding)
	if extTableDef.Type == READABLE || extTableDef.Type == READABLE_WEB {
		metadataFile.MustPrint(generateLogErrorStatement(extTableDef))
	}
}

func PrintCreateExternalProtocolStatement(metadataFile *utils.FileWithByteCount, objToc *toc.TOC, protocol ExternalProtocol, funcInfoMap map[uint32]FunctionInfo, protoMetadata ObjectMetadata) {
	start := metadataFile.ByteCount
	funcOidList := []uint32{protocol.ReadFunction, protocol.WriteFunction, protocol.Validator}
	hasUserDefinedFunc := false
	tier := globalTierMap[protocol.GetUniqueID()]
	for _, funcOid := range funcOidList {
		if funcInfo, ok := funcInfoMap[funcOid]; ok && !funcInfo.IsInternal {
			hasUserDefinedFunc = true
		}
	}
	if hasUserDefinedFunc {
		protocolFunctions := make([]string, 0)
		if protocol.ReadFunction != 0 {
			protocolFunctions = append(protocolFunctions, fmt.Sprintf("readfunc = %s", funcInfoMap[protocol.ReadFunction].QualifiedName))
		}
		if protocol.WriteFunction != 0 {
			protocolFunctions = append(protocolFunctions, fmt.Sprintf("writefunc = %s", funcInfoMap[protocol.WriteFunction].QualifiedName))
		}
		if protocol.Validator != 0 {
			protocolFunctions = append(protocolFunctions, fmt.Sprintf("validatorfunc = %s", funcInfoMap[protocol.Validator].QualifiedName))
		}

		metadataFile.MustPrintf("\n\nCREATE ")
		if protocol.Trusted {
			metadataFile.MustPrintf("TRUSTED ")
		}
		metadataFile.MustPrintf("PROTOCOL %s (%s);\n", protocol.Name, strings.Join(protocolFunctions, ", "))

		section, entry := protocol.GetMetadataEntry()
		objToc.AddMetadataEntry(section, entry, start, metadataFile.ByteCount, tier)
	}
	PrintObjectMetadata(metadataFile, objToc, protoMetadata, protocol, "", tier)
}

func PrintExchangeExternalPartitionStatements(metadataFile *utils.FileWithByteCount, objToc *toc.TOC, extPartitions []PartitionInfo, partInfoMap map[uint32]PartitionInfo, tables []Table) {
	tableNameMap := make(map[uint32]string, len(tables))
	for _, table := range tables {
		tableNameMap[table.Oid] = table.FQN()
	}
	for _, externalPartition := range extPartitions {
		extPartRelationName := tableNameMap[externalPartition.RelationOid]
		if extPartRelationName == "" {
			continue // Not included in the list of tables to back up
		}
		parentRelationName := utils.MakeFQN(externalPartition.ParentSchema, externalPartition.ParentRelationName)
		start := metadataFile.ByteCount
		alterPartitionStr := ""
		maxParentTier := []uint32{0, 0}
		currentPartition := externalPartition
		for currentPartition.PartitionParentRuleOid != 0 {
			parent := partInfoMap[currentPartition.PartitionParentRuleOid]
			if parent.PartitionName == "" {
				alterPartitionStr = fmt.Sprintf("ALTER PARTITION FOR (RANK(%d)) ", parent.PartitionRank) + alterPartitionStr
			} else {
				// Keep track of deepest tier for partition parents, to ensure that the exchange
				// statement is always run after the parent exists
				parentTier, ok := globalTierMap[UniqueID{ClassID: PG_CLASS_OID, Oid: parent.RelationOid}]
				if ok && parentTier[0] > maxParentTier[0] {
					maxParentTier = parentTier
				}

				alterPartitionStr = fmt.Sprintf("ALTER PARTITION %s ", parent.PartitionName) + alterPartitionStr
			}
			currentPartition = parent
		}
		metadataFile.MustPrintf("\n\nALTER TABLE %s %s", parentRelationName, alterPartitionStr)
		if externalPartition.PartitionName == "" {
			metadataFile.MustPrintf("EXCHANGE PARTITION FOR (RANK(%d)) ", externalPartition.PartitionRank)
		} else {
			metadataFile.MustPrintf("EXCHANGE PARTITION %s ", externalPartition.PartitionName)
		}
		metadataFile.MustPrintf("WITH TABLE %s WITHOUT VALIDATION;", extPartRelationName)
		metadataFile.MustPrintf("\n\nDROP TABLE %s;", extPartRelationName)

		section, entry := externalPartition.GetMetadataEntry()
		objToc.AddMetadataEntry(section, entry, start, metadataFile.ByteCount, []uint32{maxParentTier[0] + 1, maxParentTier[1]})
	}
}
