package entity

import (
	"fmt"
	"path"
	"path/filepath"
	"strings"
	"time"

	"github.com/brevdev/brev-cli/pkg/collections"
)

// CredentialProvider describes which authentication system is resposnible for auth tokens.
type CredentialProvider string

const (
	CredientialProviderUnspecified CredentialProvider = ""
)

const WorkspaceGroupDevPlane = "devplane-brev-1"

var LegacyWorkspaceGroups = map[string]bool{
	"k8s.brevstack.com":            true,
	"brev-test-brevtenant-cluster": true,
}

type AuthTokens struct {
	AccessToken  string `json:"access_token"`
	RefreshToken string `json:"refresh_token"`
}

type IDEConfig struct {
	DefaultWorkingDir string       `json:"defaultWorkingDir"`
	VSCode            VSCodeConfig `json:"vscode"`
} // @Name IDEConfig

type VSCodeConfig struct {
	Extensions []VscodeExtensionMetadata `json:"extensions"`
} // @Name VSCodeConfig

type VscodeExtensionMetadata struct {
	Name        string `json:"name"`
	DisplayName string `json:"displayName"`
	Version     string `json:"version"`
	Publisher   string `json:"publisher"`
	Description string `json:"description"`
	Repository  string `json:"repository"`
} // @Name ExtensionMetadata

type (
	RepoName string
	RepoV0   struct {
		Repository    string   `json:"repository"`
		Branch        string   `json:"branch"` // branch, tag, commit
		Directory     string   `json:"directory"`
		BrevPath      string   `json:"brevPath"`
		SetupExecPath string   `json:"setupExecPath"`
		ExecWorkDir   string   `json:"execWorkDir"`
		DependsOn     []string `json:"dependsOn"`
	}
	ReposV0 map[RepoName]RepoV0
	RepoV1  struct {
		Type RepoType `json:"type"`
		GitRepo
		EmptyRepo
	}
	ReposV1 map[RepoName]RepoV1
)
type RepoType string

func (r RepoV1) GetDir() (string, error) {
	if r.Type == GitRepoType { //nolint:gocritic // i like ifs
		return r.GitRepo.GetDir(), nil
	} else if r.Type == EmptyRepoType {
		return *r.EmptyDirectory, nil
	} else {
		return "", fmt.Errorf("error: invalid type in getting dir of repov1")
	}
}

const (
	GitRepoType   RepoType = "git"
	EmptyRepoType RepoType = "empty"
)

type GitRepo struct {
	Repository string `json:"repository,omitempty"`
	GitRepoOptions
}

func (g GitRepo) GetDir() string {
	if g.GitDirectory != nil && *g.GitDirectory != "" {
		return *g.GitDirectory
	} else {
		repo := g.Repository
		return strings.Split(repo[strings.LastIndex(repo, "/")+1:], ".")[0]
	}
}

type GitRepoOptions struct {
	Branch       *string `json:"branch,omitempty"`           // branch, tag, commit
	GitDirectory *string `json:"gitRepoDirectory,omitempty"` // need to be different names than emptyrepo
}
type EmptyRepo struct {
	EmptyDirectory *string `json:"emptyRepoDirectory,omitempty"` // need to be different names than gitrepo
}

type (
	ExecName string
	ExecV0   struct {
		Exec        string   `json:"exec"`
		ExecWorkDir string   `json:"execWorkDir"`
		DependsOn   []string `json:"dependsOn"`
	}
	ExecsV0 map[ExecName]ExecV0
	ExecV1  struct {
		Type  ExecType   `json:"type"`  // string or path // default=str
		Stage *ExecStage `json:"stage"` // start, build // default=start
		ExecOptions
		StringExec
		PathExec
	}
	ExecOptions struct {
		IsDisabled     bool       `json:"isDisabled"`
		ExecWorkDir    *string    `json:"execWorkDir"`
		LogPath        *string    `json:"logPath"`
		LogArchivePath *string    `json:"logArchivePath"`
		DependsOn      []ExecName `json:"dependsOn"`
	}
	ExecsV1 map[ExecName]ExecV1
)

type ExecType string

const StringExecType ExecType = "string"

type StringExec struct {
	ExecStr string `json:"execStr,omitempty"`
}

const PathExecType ExecType = "path"

type PathExec struct {
	ExecPath string `json:"execPath,omitempty"`
}

type ExecStage string

const (
	StartStage ExecStage = "start"
	BuildStage ExecStage = "build"
)

type UpdateUser struct {
	Username          string                 `json:"username,omitempty"`
	Name              string                 `json:"name,omitempty"`
	Email             string                 `json:"email,omitempty"`
	BaseWorkspaceRepo string                 `json:"baseWorkspaceRepo,omitempty"`
	OnboardingData    map[string]interface{} `json:"onboardingData,omitempty"`
	IdeConfig         IDEConfig              `json:"ideConfig,omitempty"`
}

type GlobalUserType string

const (
	Admin    GlobalUserType = "Admin"
	Standard GlobalUserType = "Standard"
)

type ExternalIdentity struct {
	IdentityID string `json:"identity_id,omitempty"`
	Provider   string `json:"provider,omitempty"`
	ExternalID string `json:"external_id,omitempty"`
}

type User struct {
	ID                 string                 `json:"id"`
	PublicKey          string                 `json:"publicKey,omitempty"`
	Username           string                 `json:"username"`
	Name               string                 `json:"name"`
	Email              string                 `json:"email"`
	WorkspacePassword  string                 `json:"workspacePassword"`
	BaseWorkspaceRepo  string                 `json:"baseWorkspaceRepo"`
	GlobalUserType     GlobalUserType         `json:"globalUserType"`
	IdeConfig          IDEConfig              `json:"ideConfig,omitempty"`
	OnboardingData     map[string]interface{} `json:"onboardingData"`
	ExternalIdentities []*ExternalIdentity    `json:"externalIdentities,omitempty"`
}

type UserKeys struct {
	PrivateKey string `json:"privateKey"`
	PublicKey  string `json:"publicKey"`
}

func (v VscodeExtensionMetadata) GetID() string {
	return fmt.Sprintf("%s.%s", v.Publisher, v.Name)
}

type Organization struct {
	ID            string `json:"id"`
	Name          string `json:"name"`
	UserNetworkID string `json:"userNetworkId"`
}

type WorkspaceMetaData struct {
	PodName       string `json:"podName"`
	NamespaceName string `json:"namespaceName"`
}

func (w WorkspaceMetaData) GetPodName() string {
	return w.PodName
}

func (w WorkspaceMetaData) GetNamespaceName() string {
	return w.NamespaceName
}

type WorkspaceWithMeta struct {
	WorkspaceMetaData
	Workspace
}

type Application struct {
	ID           string `json:"id"`
	Name         string `json:"name"`
	Port         int    `json:"port"`
	StartCommand string `json:"startCommand"`
	Version      string `json:"version"`
}

type RequestCreateWorkspace struct {
	Name                 string        `json:"name"`
	WorkspaceGroupID     string        `json:"workspaceGroupId"`
	WorkspaceClassID     string        `json:"workspaceClassId"`
	GitRepo              string        `json:"gitRepo"`
	IsStoppable          bool          `json:"isStoppable"`
	WorkspaceTemplateID  string        `json:"workspaceTemplateId"`
	PrimaryApplicationID string        `json:"primaryApplicationId"`
	Applications         []Application `json:"applications"`
}

type WorkspaceLocalID string

// Workspace Status
const (
	Running   = "RUNNING"
	Starting  = "STARTING"
	Stopping  = "STOPPING"
	Deploying = "DEPLOYING"
	Stopped   = "STOPPED"
	Deleting  = "DELETING"
	Failure   = "FAILURE"
)

// Health Status
const (
	Healthy     = "HEALTHY"
	Unhealthy   = "UNHEALTHY"
	Unavailable = "UNAVAILABLE"
)

// Shell Status
const (
	NotReady = "NOT READY"
	Ready    = "READY"
)

type WorkspaceGroup struct {
	ID             string `json:"id"`
	Name           string `json:"name"`
	BaseDNS        string `json:"baseDns"`
	Status         string `json:"status"`
	Platform       string `json:"platform"`
	PlatformID     string `json:"platformId"`
	PlatformRegion string `json:"platformRegion"`
	Version        string `json:"version"`
	TenantType     string `json:"tenantType"`
} // @Name WorkspaceGroup

type Workspace struct {
	ID                   string            `json:"id"`
	Name                 string            `json:"name"`
	WorkspaceGroupID     string            `json:"workspaceGroupId"`
	OrganizationID       string            `json:"organizationId"`
	WorkspaceClassID     string            `json:"workspaceClassId"` // WorkspaceClassID is resources, like "2x8"
	InstanceType         string            `json:"instanceType,omitempty"`
	CreatedByUserID      string            `json:"createdByUserId"`
	DNS                  string            `json:"dns"`
	Status               string            `json:"status"`
	Password             string            `json:"password"`
	GitRepo              string            `json:"gitRepo"`
	Version              string            `json:"version"`
	WorkspaceTemplate    WorkspaceTemplate `json:"workspaceTemplate"`
	NetworkID            string            `json:"networkId"`
	StartupScriptPath    string            `json:"startupScriptPath"`
	ReposV0              ReposV0           `json:"repos"`
	ExecsV0              ExecsV0           `json:"execs"`
	ReposV1              *ReposV1          `json:"reposV1"`
	ExecsV1              *ExecsV1          `json:"execsV1"`
	IDEConfig            IDEConfig         `json:"ideConfig"`
	SSHPort              int               `json:"sshPort"`
	SSHUser              string            `json:"sshUser"`
	SSHProxyHostname     string            `json:"sshProxyHostname"`
	HostSSHPort          int               `json:"hostSshPort"`
	HostSSHUser          string            `json:"hostSshUser"`
	HostSSHProxyHostname string            `json:"hostSshProxyHostname"`
	VerbBuildStatus      VerbBuildStatus   `json:"verbBuildStatus"`
	VerbYaml             string            `json:"verbYaml"`
	// PrimaryApplicationId         string `json:"primaryApplicationId,omitempty"`
	// LastOnlineAt         string `json:"lastOnlineAt,omitempty"`
	// CreatedAt         string `json:"createdAt,omitempty"`
	// UpdatedAt         string `json:"updatedAt,omitempty"`
	HealthStatus    string        `json:"healthStatus"`
	IsStoppable     bool          `json:"isStoppable"` // used for autopstop only
	StatusMessage   string        `json:"statusMessage"`
	StopTimeout     time.Duration `json:"stopTimeout"`
	AdditionalUsers []string      `json:"additionalUsers"`
	Tunnel          Tunnel        `json:"tunnel"`
}

type APIKey struct {
	Enabled      bool   `json:"enabled"`
	ID           string `json:"id"`
	ClientID     string `json:"clientID"`
	ClientSecret string `json:"clientSecret"`
}

type Policy struct {
	AllowedUserAuthIDs []string `json:"allowedUserAuthIDs"`
	AllowEveryone      bool     `json:"allowEveryone"`
	APIKey             APIKey   `json:"apiKey"`
}

type Tunnel struct {
	TunnelID        string          `json:"tunnelID"`
	Applications    []CFApplication `json:"applications"`
	TunnelSetupBash string          `json:"tunnelSetupBash"`
	TunnelStatus    string          `json:"tunnelStatus"`
}

// CFApplication TODO Change this to Application. Theres an older application struct that should be removed
type CFApplication struct {
	CloudflareApplicationID  string `json:"cloudflareApplicationID"`
	CloudflareDNSRecordID    string `json:"cloudflareDnsRecordID"`
	Hostname                 string `json:"hostname"`
	Name                     string `json:"name"`
	ServiceType              string `json:"serviceType"`
	Port                     int    `json:"port"`
	UserApplicationSetupBash string `json:"userApplicationSetupBash"`
	Policy                   Policy `json:"policy"`
	HealthCheckID            string `json:"healthCheckID"`
}
type VerbBuildStatus string

const (
	Unset        VerbBuildStatus = ""
	CreateFailed VerbBuildStatus = "CREATE_FAILED"
	Pending      VerbBuildStatus = "PENDING"
	Building     VerbBuildStatus = "BUILDING"
	Completed    VerbBuildStatus = "COMPLETED"
)

func (w Workspace) GetStopTimeout() time.Duration {
	return w.StopTimeout
}

func (w Workspace) GetIsStoppable() bool {
	return w.IsStoppable
}

func (w Workspace) CanShow(userID string) bool {
	return w.CreatedByUserID == userID || (w.AdditionalUsers != nil && collections.ListContains(w.AdditionalUsers, userID))
}

func (w Workspace) IsShared(userID string) bool {
	return w.CreatedByUserID != userID && (w.AdditionalUsers != nil && collections.ListContains(w.AdditionalUsers, userID))
}

func (w Workspace) GetHostname() string {
	hostname := w.DNS
	if hostname == "" {
		hostname = "-"
	}
	return hostname
}

func (w Workspace) GetSSHPort() int {
	port := 22
	if w.SSHPort != 0 {
		port = w.SSHPort
	}
	return port
}

const DefaultUser = "ubuntu"

func (w Workspace) GetSSHUser() string {
	user := DefaultUser
	if w.SSHUser != "" {
		user = w.SSHUser
	}
	return user
}

func (w Workspace) GetHostSSHUser() string {
	user := DefaultUser
	if w.HostSSHUser != "" {
		user = w.HostSSHUser
	}
	return user
}

func (w Workspace) GetHostSSHPort() int {
	port := 22
	if w.HostSSHPort != 0 {
		port = w.HostSSHPort
	}
	return port
}

func (w Workspace) IsLegacy() bool {
	return MapContainsKey(LegacyWorkspaceGroups, w.WorkspaceGroupID)
}

func (w Workspace) GetUsername() string {
	if w.IsLegacy() {
		return "brev"
	}
	return DefaultUser
}

type WorkspaceTemplate struct {
	ID          string `json:"id"`
	Type        string `json:"type"`
	Name        string `json:"name"`
	RegistryURI string `json:"registryUri"`
	Image       string `json:"image"`
	Public      bool   `json:"public"`
	Port        int    `json:"port"`
}

func MapContainsKey[K comparable, V any](m map[K]V, key K) bool {
	_, ok := m[key]
	return ok
}

func (w Workspace) GetProjectFolderPath() (string, error) {
	var prefix string
	if MapContainsKey(LegacyWorkspaceGroups, w.WorkspaceGroupID) {
		prefix = "/home/brev/workspace"
	} else {
		if w.SSHUser == "" {
			return "", fmt.Errorf("workspace %s has empty SSH user (workspace may not be running)", w.ID)
		}
		prefix = "/home/" + w.SSHUser
	}
	var folderName string
	if w.IDEConfig.DefaultWorkingDir != "" { //nolint:gocritic // i like if else
		if path.IsAbs(w.IDEConfig.DefaultWorkingDir) {
			return w.IDEConfig.DefaultWorkingDir, nil
		} else {
			folderName = w.IDEConfig.DefaultWorkingDir
		}
	} else if w.GitRepo != "" {
		folderName = GetDefaultProjectFolderNameFromRepo(w.GitRepo)
	} else if w.ReposV1 != nil {
		// on reposV1 but have no default working dir
		if len(*w.ReposV1) == 1 {
			for _, v := range *w.ReposV1 {
				if v.Type == GitRepoType {
					folderName = *v.GitDirectory
				} else {
					folderName = *v.EmptyDirectory
				}
			}
		} else {
			return prefix, nil
		}
	} else {
		return prefix, nil
	}

	return filepath.Join(prefix, folderName), nil // TODO make workspace dir configurable
}

func GetDefaultProjectFolderNameFromRepo(repo string) string {
	return strings.Split(repo[strings.LastIndex(repo, "/")+1:], ".")[0]
}

func (w Workspace) GetLocalIdentifier() WorkspaceLocalID {
	return w.createSimpleName()
}

func (w Workspace) GetHostIdentifier() WorkspaceLocalID {
	return w.createSimpleName() + "-host"
}

// lowercase, replace whitespace with '-', remove all [^a-z0-9-], trim '-' front and back

func (w Workspace) GetID() string {
	return w.ID
}

func (w Workspace) GetSSHURL() string {
	return "ssh-" + w.DNS
}

func (w Workspace) createSimpleName() WorkspaceLocalID {
	return WorkspaceLocalID(w.Name)
}

func (w Workspace) GetNodeIdentifierForVPN() string {
	return string(w.createSimpleName())
}

type OnboardingData struct {
	Editor                 string `json:"editor"`
	SSH                    bool   `json:"ssh"`
	UsedCLI                bool   `json:"usedCli"`
	FinishedOnboarding     bool   `json:"finishedOnboarding"`
	CliOnboardingSkipped   bool   `json:"cliOnboardingSkipped"`
	CliOnboardingIntro     bool   `json:"cliOnboardingIntro"`
	CliOnboardingLs        bool   `json:"cliOnboardingLs"`
	CliOnboardingBrevOpen  bool   `json:"cliOnboardingBrevOpen"`
	CliOnboardingBrevShell bool   `json:"cliOnboardingBrevShell"`
	CliOnboardingCompleted bool   `json:"cliOnboardingCompleted"`
}

// https://stackoverflow.com/questions/27545270/how-to-get-a-value-from-map/
func safeStringMap(mapStrInter map[string]interface{}, key, fallback string) string {
	var value string
	var ok bool
	if x, found := mapStrInter[key]; found {
		if value, ok = x.(string); !ok {
			// do whatever you want to handle errors - this means this wasn't a string
			return fallback
		}
		return value
	}
	return fallback
}

// https://stackoverflow.com/questions/27545270/how-to-get-a-value-from-map/
// return false if map key does not exist
func safeFalseBoolMap(mapStrInter map[string]interface{}, key string) bool {
	var value bool
	var ok bool
	if x, found := mapStrInter[key]; found {
		if value, ok = x.(bool); !ok {
			return false
		}
		return value
	}
	return false
}

func (u User) GetOnboardingData() (*OnboardingData, error) {
	x := &OnboardingData{
		Editor:                 safeStringMap(u.OnboardingData, "editor", ""), // empty string is the false state here
		SSH:                    safeFalseBoolMap(u.OnboardingData, "SSH"),
		UsedCLI:                safeFalseBoolMap(u.OnboardingData, "usedCLI"),
		FinishedOnboarding:     safeFalseBoolMap(u.OnboardingData, "finishedOnboarding"),
		CliOnboardingSkipped:   safeFalseBoolMap(u.OnboardingData, "cliOnboardingSkipped"),
		CliOnboardingIntro:     safeFalseBoolMap(u.OnboardingData, "cliOnboardingIntro"),
		CliOnboardingLs:        safeFalseBoolMap(u.OnboardingData, "cliOnboardingLs"),
		CliOnboardingBrevOpen:  safeFalseBoolMap(u.OnboardingData, "cliOnboardingBrevOpen"),
		CliOnboardingBrevShell: safeFalseBoolMap(u.OnboardingData, "cliOnboardingBrevShell"),
		CliOnboardingCompleted: safeFalseBoolMap(u.OnboardingData, "cliOnboardingCompleted"),
	}
	return x, nil
}

type ModifyWorkspaceRequest struct {
	WorkspaceClass    string    `json:"workspaceClassId"`
	IsStoppable       *bool     `json:"isStoppable"`
	StartupScriptPath string    `json:"startupScriptPath"`
	Name              string    `json:"name"`
	Description       string    `json:"description"`
	IDEConfig         IDEConfig `json:"ideConfig"`
	Repos             ReposV0   `json:"repos"`
	Execs             ExecsV0   `json:"execs"`
	ReposV1           *ReposV1  `json:"reposV1"`
	ExecsV1           *ExecsV1  `json:"execsV1"`
	InstanceType      string    `json:"instanceType"`
}
