package entity

import (
	"database/sql/driver"
	"encoding/json"
	"sort"
	"strings"
	"time"

	"github.com/pingcap/tidb/pkg/parser/charset"
	"github.com/pingcap/tidb/pkg/parser/mysql"
	types2 "github.com/pingcap/tidb/pkg/parser/types"
	"github.com/pingcap/tidb/pkg/types"
	"github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/serverless-service/internal/domain/constant"
	tidbmgmtservicev1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
)

type Import struct {
	ID        string                `gorm:"column:id;type:varchar(255);not null;primaryKey"`
	Name      string                `gorm:"column:name;type:varchar(255);not null"`
	IDCompat  uint64                `gorm:"column:id_compat;type:BIGINT UNSIGNED NOT NULL;index"`
	CreatedAt time.Time             `gorm:"created_at;type:TIMESTAMP;not null"` // create and read only
	UpdatedAt *time.Time            `gorm:"updated_at;type:TIMESTAMP"`
	Status    constant.ImportStatus `gorm:"type:ENUM('unknown','preparing','importing','completed','failed','canceling','canceled');not null;default:'preparing'"` // enum

	StartedAt   *time.Time `gorm:"type:TIMESTAMP"`
	CompletedAt *time.Time `gorm:"type:TIMESTAMP;index"`

	Creator         string `gorm:"type:VARCHAR(255)"`
	ClusterID       string `gorm:"index;not null"                                             valid:"required"`
	ClusterStatusID uint64 `gorm:"column:cluster_status_id;type:bigint(20) NULL DEFAULT NULL"`
	// ProjectID stores the OriginalProjectId (stable, not changed during project migration).
	// It is used for Infra CR interaction only (e.g., namespace, labels).
	// DO NOT use this for events, RPC, HTTP responses, or any user-facing APIs.
	// For those cases, always use cluster.ProjectId to get the latest project id.
	ProjectID string `gorm:"not null"`
	TenantID  string `gorm:"not null"`
	Message   string `gorm:"type:TEXT"` // failed message

	CleanUp bool `gorm:"index;default:false;not null"`

	BucketRegion   string `gorm:"type:VARCHAR(255)"`
	DataFormat     constant.DataFormat
	DataSourceType constant.DataSource

	CSVFormat string `gorm:"type:VARCHAR(1024)"`

	// Source data info
	TotalSize          *uint64
	TotalSizeWithIndex *uint64
	ProcessedDataSize  *uint64
	TotalFiles         *uint32
	SourceURL          string        `gorm:"type:VARCHAR(1024)"`
	CurrentTables      CurrentTables `gorm:"type:JSON"`

	// In progress info
	TotalTablesCount           uint32
	CompletedTables            uint32
	PendingTables              uint32
	CompletedPercent           uint32 // 0 ~100
	PostImportCompletedPercent uint32 // 0 ~100
	TaskID                     int64

	// Table results
	TableResults ImportTables `gorm:"type:JSON"`

	LocalImportCfg *LocalImportCfg `gorm:"type:JSON"`

	S3RoleArn         string `gorm:"column:s3_role_arn;type:varchar(2048);null"`
	S3AccessKeyID     string `gorm:"column:s3_access_key_id;type:varchar(255);null"`
	S3SecretAccessKey string `gorm:"column:s3_secret_access_key;type:varchar(1024);null"`
	S3Endpoint        string `gorm:"column:s3_endpoint;type:varchar(255);null"`

	AzureSASToken string `gorm:"column:azure_sas_token;type:TEXT;null"`

	GCSCredentials string `gorm:"column:gcs_credentials;type:TEXT;null"`

	TargetTableInfos TargetTableInfos `gorm:"type:JSON"`

	NeedDropTables bool `gorm:"type:tinyint(1)"`
}

type CurrentTable struct {
	Name             string `json:"name"`
	Size             uint64 `json:"size"`
	CompletedPercent uint32 `json:"completed_percent"` // 0 ~100
}

type CurrentTables struct {
	Tables []*CurrentTable `json:"tables"`
}

// Value marshal json
func (c CurrentTables) Value() (driver.Value, error) {
	b, err := json.Marshal(c)
	return string(b), err
}

// Scan unmarshal json
func (c *CurrentTables) Scan(input any) error {
	return json.Unmarshal(input.([]byte), c)
}

type ImportTable struct {
	TableName string
	Result    constant.ImportTableResult
	Message   string
}

type ImportTables struct {
	Tables []ImportTable
}

func (r ImportTables) Value() (driver.Value, error) {
	b, err := json.Marshal(r)
	return string(b), err
}

func (r *ImportTables) Scan(input any) error {
	return json.Unmarshal(input.([]byte), r)
}

type LocalImportCfg struct {
	FileName    string `json:"file_name"`
	TargetDB    string `json:"target_db"`
	TargetTable string `json:"target_table"`
	UploadID    string `json:"upload_id"`
}

func (c LocalImportCfg) Value() (driver.Value, error) {
	b, err := json.Marshal(c)
	return string(b), err
}

func (c *LocalImportCfg) Scan(input any) error {
	return json.Unmarshal(input.([]byte), c)
}

type TargetTableInfos struct {
	Infos []TargetTableInfo
}

type TargetTableInfo struct {
	Schema     string `json:"schema"`
	Table      string `json:"table"`
	CustomFile string `json:"custom_file"`
}

func (t TargetTableInfos) Value() (driver.Value, error) {
	b, err := json.Marshal(t)
	return string(b), err
}

func (t *TargetTableInfos) Scan(input any) error {
	return json.Unmarshal(input.([]byte), t)
}

func IsEndStatus(status constant.ImportStatus) bool {
	switch status {
	case constant.ImportStatusCompleted,
		constant.ImportStatusFailed,
		constant.ImportStatusCanceled:
		return true
	default:
		return false
	}
}

// ImportTask is the entity object to manipulate in model.
type ImportTask struct {
	*Import
	// Cluster stores the cluster information for the imported cluster.
	Cluster *tidbmgmtservicev1.Cluster
	Org     *account.Org
}

type ColumnInfo struct {
	ColumnName string
	ColumnType string
}

type ImportFilter struct {
	ClusterID        string
	PageSize         *int32
	Offset           *int64
	Statuses         []constant.ImportStatus
	CreatedAtAfter   *time.Time
	CompletedAtAfter *time.Time
	CleanUp          *bool
	OrderBy          string
}

type DataIngestionProfile struct {
	ExternalId string
	AccountId  string
}

// ImportProviderType returns the dataplane provider-type label for import
// metrics. Empty/legacy records fall back to aws-free-tier.
func ImportProviderType(cluster *tidbmgmtservicev1.Cluster) string {
	if cluster == nil {
		return constant.ProviderTypeAWSFreeTier
	}
	if pt := cluster.GetServerlessCluster().GetCluster().GetProviderType(); pt != "" {
		return pt
	}
	return constant.FreeTierProviderType(cluster.CloudProvider)
}

func GetDefaultDataType() []string {
	dataTypes := []*types.FieldType{ // corresponding string format
		types2.NewFieldType(mysql.TypeBit),               // bit
		types2.NewFieldType(mysql.TypeBlob),              // text
		types2.NewFieldType(mysql.TypeDate),              // date
		types2.NewFieldType(mysql.TypeDatetime),          // datetime
		types2.NewFieldType(mysql.TypeFloat),             // float
		types2.NewFieldType(mysql.TypeDouble),            // double
		types2.NewFieldType(mysql.TypeInt24),             // mediumint
		types2.NewFieldType(mysql.TypeJSON),              // json
		types2.NewFieldType(mysql.TypeLong),              // int
		types2.NewFieldType(mysql.TypeLonglong),          // bigint
		types2.NewFieldType(mysql.TypeLongBlob),          // longtext
		types2.NewFieldType(mysql.TypeMediumBlob),        // mediumtext
		types2.NewFieldType(mysql.TypeShort),             // smallint
		types2.NewFieldType(mysql.TypeDuration),          // time
		types2.NewFieldType(mysql.TypeTimestamp),         // timestamp
		types2.NewFieldType(mysql.TypeTiny),              // tinyint
		types2.NewFieldType(mysql.TypeTinyBlob),          // tinytext
		types2.NewFieldType(mysql.TypeTiDBVectorFloat32), // vector
	}

	// year(4)
	dataTypes = append(dataTypes, types2.NewFieldType(mysql.TypeYear))
	dataTypes[len(dataTypes)-1].SetFlen(4)

	// boolean:tinyint(1)
	dataTypes = append(dataTypes, types2.NewFieldType(mysql.TypeTiny))
	dataTypes[len(dataTypes)-1].SetFlen(1)

	// decimal[32[,4]]
	dataTypes = append(dataTypes, types2.NewFieldType(mysql.TypeNewDecimal))
	dataTypes[len(dataTypes)-1].SetFlen(32)
	dataTypes[len(dataTypes)-1].SetDecimal(4)

	// char(32)
	dataTypes = append(dataTypes, types2.NewFieldType(mysql.TypeString))
	dataTypes[len(dataTypes)-1].SetFlen(32)
	// varchar(64)
	dataTypes = append(dataTypes, types2.NewFieldType(mysql.TypeVarchar))
	dataTypes[len(dataTypes)-1].SetFlen(64)
	// varchar(256)
	dataTypes = append(dataTypes, types2.NewFieldType(mysql.TypeVarchar))
	dataTypes[len(dataTypes)-1].SetFlen(256)
	// varchar(512)
	dataTypes = append(dataTypes, types2.NewFieldType(mysql.TypeVarchar))
	dataTypes[len(dataTypes)-1].SetFlen(512)

	// binary(32)
	dataTypes = append(dataTypes, types2.NewFieldType(mysql.TypeString))
	dataTypes[len(dataTypes)-1].SetFlen(32)
	dataTypes[len(dataTypes)-1].SetCharset(charset.CharsetBin)
	// varbinary(64)
	dataTypes = append(dataTypes, types2.NewFieldType(mysql.TypeVarchar))
	dataTypes[len(dataTypes)-1].SetFlen(64)
	dataTypes[len(dataTypes)-1].SetCharset(charset.CharsetBin)
	// varbinary(256)
	dataTypes = append(dataTypes, types2.NewFieldType(mysql.TypeVarchar))
	dataTypes[len(dataTypes)-1].SetFlen(256)
	dataTypes[len(dataTypes)-1].SetCharset(charset.CharsetBin)
	// varbinary(512)
	dataTypes = append(dataTypes, types2.NewFieldType(mysql.TypeVarchar))
	dataTypes[len(dataTypes)-1].SetFlen(512)
	dataTypes[len(dataTypes)-1].SetCharset(charset.CharsetBin)

	res := make([]string, len(dataTypes))
	for i := range dataTypes {
		res[i] = strings.ToUpper(dataTypes[i].CompactStr())
	}
	sort.StringSlice(res).Sort()
	return res
}
