package utils

import (
	"os"
	"path/filepath"
	"regexp"

	"github.com/go-errors/errors"
	"github.com/spf13/afero"
	"github.com/supabase/cli/internal/utils/credentials"
	"github.com/zalando/go-keyring"
)

var (
	AccessTokenPattern = regexp.MustCompile(`^sbp_(oauth_)?[a-f0-9]{40}$`)
	ErrInvalidToken    = errors.New("Invalid access token format. Must be like `sbp_0102...1920`.")
	ErrMissingToken    = errors.Errorf("Access token not provided. Supply an access token by running %s or setting the SUPABASE_ACCESS_TOKEN environment variable.", Aqua("supabase login"))
	ErrNotLoggedIn     = errors.New("You were not logged in, nothing to do.")
)

const AccessTokenKey = "access-token"

func LoadAccessToken() (string, error) {
	return LoadAccessTokenFS(afero.NewOsFs())
}

func LoadAccessTokenFS(fsys afero.Fs) (string, error) {
	accessToken, err := loadAccessToken(fsys)
	if err != nil {
		return "", err
	}
	if !AccessTokenPattern.MatchString(accessToken) {
		return "", errors.New(ErrInvalidToken)
	}
	return accessToken, nil
}

func loadAccessToken(fsys afero.Fs) (string, error) {
	// Env takes precedence
	if accessToken := os.Getenv("SUPABASE_ACCESS_TOKEN"); accessToken != "" {
		return accessToken, nil
	}
	// Load from native credentials store
	if accessToken, err := credentials.StoreProvider.Get(AccessTokenKey); err == nil {
		return accessToken, nil
	}
	// Fallback to token file
	return fallbackLoadToken(fsys)
}

func fallbackLoadToken(fsys afero.Fs) (string, error) {
	path, err := getAccessTokenPath()
	if err != nil {
		return "", err
	}
	accessToken, err := afero.ReadFile(fsys, path)
	if errors.Is(err, os.ErrNotExist) {
		return "", errors.New(ErrMissingToken)
	} else if err != nil {
		return "", errors.Errorf("failed to read access token file: %w", err)
	}
	return string(accessToken), nil
}

func SaveAccessToken(accessToken string, fsys afero.Fs) error {
	// Validate access token
	if !AccessTokenPattern.MatchString(accessToken) {
		return errors.New(ErrInvalidToken)
	}
	// Save to native credentials store
	if err := credentials.StoreProvider.Set(AccessTokenKey, accessToken); err == nil {
		return nil
	}
	// Fallback to token file
	return fallbackSaveToken(accessToken, fsys)
}

func fallbackSaveToken(accessToken string, fsys afero.Fs) error {
	path, err := getAccessTokenPath()
	if err != nil {
		return err
	}
	if err := MkdirIfNotExistFS(fsys, filepath.Dir(path)); err != nil {
		return err
	}
	if err := afero.WriteFile(fsys, path, []byte(accessToken), 0600); err != nil {
		return errors.Errorf("failed to save access token file: %w", err)
	}
	return nil
}

func DeleteAccessToken(fsys afero.Fs) error {
	// Always delete the fallback token file to handle legacy CLI
	if err := fallbackDeleteToken(fsys); err == nil {
		// Typically user system should only have either token file or keyring.
		// But we delete from both just in case.
		_ = credentials.StoreProvider.Delete(AccessTokenKey)
		return nil
	} else if !errors.Is(err, os.ErrNotExist) {
		return err
	}
	// Fallback not found, delete from native credentials store
	err := credentials.StoreProvider.Delete(AccessTokenKey)
	if errors.Is(err, credentials.ErrNotSupported) || errors.Is(err, keyring.ErrNotFound) {
		return errors.New(ErrNotLoggedIn)
	} else if err != nil {
		return errors.Errorf("failed to delete access token from keyring: %w", err)
	}
	return nil
}

func fallbackDeleteToken(fsys afero.Fs) error {
	path, err := getAccessTokenPath()
	if err != nil {
		return err
	}
	if err := fsys.Remove(path); err != nil {
		return errors.Errorf("failed to remove access token file: %w", err)
	}
	return nil
}

func getAccessTokenPath() (string, error) {
	home, err := os.UserHomeDir()
	if err != nil {
		return "", errors.Errorf("failed to get $HOME directory: %w", err)
	}
	// TODO: fallback to workdir
	return filepath.Join(home, ".supabase", AccessTokenKey), nil
}
