package spec

import (
	"database/sql"
	"encoding/json"
	"fmt"
	"log"
	"os"
	"strings"
	"time"

	"github.com/sirupsen/logrus"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/rest"
)

// NamespacedName comprises a resource name, with a mandatory namespace,
// rendered as "<namespace>/<name>".  Being a type captures intent and
// helps make sure that UIDs, namespaced names and non-namespaced names
// do not get conflated in code.  For most use cases, namespace and name
// will already have been format validated at the API entry point, so we
// don't do that here.  Where that's not the case (e.g. in testing),
// consider using NamespacedNameOrDie() in testing.go in this package.
//
// from: https://github.com/kubernetes/apimachinery/blob/master/pkg/types/namespacedname.go
type NamespacedName struct {
	Namespace string `json:"namespace,omitempty"`
	Name      string `json:"name"`
}

const (
	Separator = '/'
)

// String returns the general purpose string representation
func (n NamespacedName) String() string {
	return n.Namespace + string(Separator) + n.Name
}

// MarshalLog emits a struct containing required key/value pair
func (n NamespacedName) MarshalLog() interface{} {
	return struct {
		Name      string `json:"name"`
		Namespace string `json:"namespace,omitempty"`
	}{
		Name:      n.Name,
		Namespace: n.Namespace,
	}
}

const fileWithNamespace = "/var/run/secrets/kubernetes.io/serviceaccount/namespace"

// RoleOrigin contains the code of the origin of a role
type RoleOrigin int

// The rolesOrigin constant values must be sorted by the role priority for
// resolveNameConflict(...) to work.
const (
	RoleOriginUnknown RoleOrigin = iota
	RoleOriginManifest
	RoleOriginInfrastructure
	RoleOriginTeamsAPI
	RoleOriginSystem
	RoleOriginBootstrap
	RoleOriginConnectionPooler
	RoleOriginStream
)

type syncUserOperation int

// Possible values for the sync user operation (removal of users is not supported yet)
const (
	PGSyncUserAdd = iota
	PGsyncUserAlter
	PGSyncAlterSet // handle ALTER ROLE SET parameter = value
	PGSyncUserRename
)

// PgUser contains information about a single user.
type PgUser struct {
	Origin     RoleOrigin        `yaml:"-"`
	Name       string            `yaml:"-"`
	Namespace  string            `yaml:"-"`
	Password   string            `yaml:"-"`
	Flags      []string          `yaml:"user_flags"`
	MemberOf   []string          `yaml:"inrole"`
	Parameters map[string]string `yaml:"db_parameters"`
	AdminRole  string            `yaml:"admin_role"`
	IsDbOwner  bool              `yaml:"is_db_owner"`
	Deleted    bool              `yaml:"deleted"`
	Rotated    bool              `yaml:"rotated"`
	Degraded   bool              `yaml:"degraded"`
}

func (user *PgUser) Valid() bool {
	return user.Name != "" && user.Password != ""
}

// PgUserMap maps user names to the definitions.
type PgUserMap map[string]PgUser

// PgSyncUserRequest has information about a single request to sync a user.
type PgSyncUserRequest struct {
	Kind syncUserOperation
	User PgUser
}

// UserSyncer defines an interface for the implementations to sync users from the manifest to the DB.
type UserSyncer interface {
	ProduceSyncRequests(dbUsers PgUserMap, newUsers PgUserMap) (req []PgSyncUserRequest)
	ExecuteSyncRequests(req []PgSyncUserRequest, db *sql.DB) error
}

// LogEntry describes log entry in the RingLogger
type LogEntry struct {
	Time        time.Time
	Level       logrus.Level
	ClusterName *NamespacedName `json:",omitempty"`
	Worker      *uint32         `json:",omitempty"`
	Message     string
}

// Diff describes diff
type Diff struct {
	EventTime   time.Time
	ProcessTime time.Time
	Diff        []string
}

// ControllerStatus describes status of the controller
type ControllerStatus struct {
	LastSyncTime    int64
	Clusters        int
	WorkerQueueSize map[int]int
}

// QueueDump describes cache.FIFO queue
type QueueDump struct {
	Keys []string
	List []interface{}
}

// ControllerConfig describes configuration of the controller
type ControllerConfig struct {
	RestConfig          *rest.Config `json:"-"`
	InfrastructureRoles map[string]PgUser

	NoDatabaseAccess     bool
	NoTeamsAPI           bool
	CRDReadyWaitInterval time.Duration
	CRDReadyWaitTimeout  time.Duration
	ConfigMapName        NamespacedName
	Namespace            string
	IgnoredAnnotations   []string

	EnableJsonLogging bool

	KubeQPS   int
	KubeBurst int
}

// cached value for the GetOperatorNamespace
var operatorNamespace string

// MarshalJSON defines marshaling rule for the namespaced name type.
func (n NamespacedName) MarshalJSON() ([]byte, error) {
	return []byte("\"" + n.String() + "\""), nil
}

// Decode converts a (possibly unqualified) string into the namespaced name object.
func (n *NamespacedName) Decode(value string) error {
	return n.DecodeWorker(value, GetOperatorNamespace())
}

// UnmarshalJSON converts a byte slice to NamespacedName
func (n *NamespacedName) UnmarshalJSON(data []byte) error {
	result := NamespacedName{}
	var tmp string
	if err := json.Unmarshal(data, &tmp); err != nil {
		return err
	}
	if err := result.Decode(tmp); err != nil {
		return err
	}
	*n = result
	return nil
}

// DecodeWorker separates the decode logic to (unit) test
// from obtaining the operator namespace that depends on k8s mounting files at runtime
func (n *NamespacedName) DecodeWorker(value, operatorNamespace string) error {
	var (
		name types.NamespacedName
	)

	result := strings.SplitN(value, string(types.Separator), 2)
	if len(result) < 2 {
		name.Name = result[0]
	} else {
		name.Name = strings.TrimLeft(result[1], string(types.Separator))
		name.Namespace = result[0]
	}
	if name.Name == "" {
		return fmt.Errorf("incorrect namespaced name: %v", value)
	}
	if name.Namespace == "" {
		name.Namespace = operatorNamespace
	}

	*n = NamespacedName(name)

	return nil
}

func (r RoleOrigin) String() string {
	switch r {
	case RoleOriginUnknown:
		return "unknown"
	case RoleOriginManifest:
		return "manifest role"
	case RoleOriginInfrastructure:
		return "infrastructure role"
	case RoleOriginTeamsAPI:
		return "teams API role"
	case RoleOriginSystem:
		return "system role"
	case RoleOriginBootstrap:
		return "bootstrapped role"
	case RoleOriginConnectionPooler:
		return "connection pooler role"
	default:
		panic(fmt.Sprintf("bogus role origin value %d", r))
	}
}

// GetOperatorNamespace assumes serviceaccount secret is mounted by kubernetes
// Placing this func here instead of pgk/util avoids circular import
func GetOperatorNamespace() string {
	if operatorNamespace == "" {
		if namespaceFromEnvironment := os.Getenv("OPERATOR_NAMESPACE"); namespaceFromEnvironment != "" {
			return namespaceFromEnvironment
		}
		operatorNamespaceBytes, err := os.ReadFile(fileWithNamespace)
		if err != nil {
			log.Fatalf("Unable to detect operator namespace from within its pod due to: %v", err)
		}
		operatorNamespace = string(operatorNamespaceBytes)
	}
	return operatorNamespace
}
