package providers

import (
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"os"
	"path"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/aliyun/credentials-go/credentials/internal/utils"
)

type CLIProfileCredentialsProvider struct {
	profileFile   string
	profileName   string
	innerProvider CredentialsProvider
	// 文件锁，用于并发安全
	fileMutex sync.RWMutex
}

type CLIProfileCredentialsProviderBuilder struct {
	provider *CLIProfileCredentialsProvider
}

func (b *CLIProfileCredentialsProviderBuilder) WithProfileFile(profileFile string) *CLIProfileCredentialsProviderBuilder {
	b.provider.profileFile = profileFile
	return b
}

func (b *CLIProfileCredentialsProviderBuilder) WithProfileName(profileName string) *CLIProfileCredentialsProviderBuilder {
	b.provider.profileName = profileName
	return b
}

func (b *CLIProfileCredentialsProviderBuilder) Build() (provider *CLIProfileCredentialsProvider, err error) {
	// 优先级：
	// 1. 使用显示指定的 profileFile
	// 2. 使用环境变量（ALIBABA_CLOUD_CONFIG_FILE）指定的 profileFile
	// 3. 兜底使用 path.Join(homeDir, ".aliyun/config") 作为 profileFile
	if b.provider.profileFile == "" {
		b.provider.profileFile = os.Getenv("ALIBABA_CLOUD_CONFIG_FILE")
	}
	// 优先级：
	// 1. 使用显示指定的 profileName
	// 2. 使用环境变量（ALIBABA_CLOUD_PROFILE）制定的 profileName
	// 3. 使用 CLI 配置中的当前 profileName
	if b.provider.profileName == "" {
		b.provider.profileName = os.Getenv("ALIBABA_CLOUD_PROFILE")
	}

	if strings.ToLower(os.Getenv("ALIBABA_CLOUD_CLI_PROFILE_DISABLED")) == "true" {
		err = errors.New("the CLI profile is disabled")
		return
	}

	provider = b.provider
	return
}

func NewCLIProfileCredentialsProviderBuilder() *CLIProfileCredentialsProviderBuilder {
	return &CLIProfileCredentialsProviderBuilder{
		provider: &CLIProfileCredentialsProvider{},
	}
}

type profile struct {
	Name                   string `json:"name"`
	Mode                   string `json:"mode"`
	AccessKeyID            string `json:"access_key_id"`
	AccessKeySecret        string `json:"access_key_secret"`
	SecurityToken          string `json:"sts_token"`
	RegionID               string `json:"region_id"`
	RoleArn                string `json:"ram_role_arn"`
	RoleSessionName        string `json:"ram_session_name"`
	DurationSeconds        int    `json:"expired_seconds"`
	StsRegion              string `json:"sts_region"`
	EnableVpc              bool   `json:"enable_vpc"`
	SourceProfile          string `json:"source_profile"`
	RoleName               string `json:"ram_role_name"`
	OIDCTokenFile          string `json:"oidc_token_file"`
	OIDCProviderARN        string `json:"oidc_provider_arn"`
	Policy                 string `json:"policy"`
	ExternalId             string `json:"external_id"`
	SignInUrl              string `json:"cloud_sso_sign_in_url"`
	AccountId              string `json:"cloud_sso_account_id"`
	AccessConfig           string `json:"cloud_sso_access_config"`
	AccessToken            string `json:"access_token"`
	AccessTokenExpire      int64  `json:"cloud_sso_access_token_expire"`
	OauthSiteType          string `json:"oauth_site_type"`
	OauthRefreshToken      string `json:"oauth_refresh_token"`
	OauthAccessToken       string `json:"oauth_access_token"`
	OauthAccessTokenExpire int64  `json:"oauth_access_token_expire"`
	StsExpire              int64  `json:"sts_expiration"`
	ProcessCommand         string `json:"process_command"`
}

type configuration struct {
	Current  string     `json:"current"`
	Profiles []*profile `json:"profiles"`
}

func newConfigurationFromPath(cfgPath string) (conf *configuration, err error) {
	bytes, err := ioutil.ReadFile(cfgPath)
	if err != nil {
		err = fmt.Errorf("reading aliyun cli config from '%s' failed %v", cfgPath, err)
		return
	}

	conf = &configuration{}

	err = json.Unmarshal(bytes, conf)
	if err != nil {
		err = fmt.Errorf("unmarshal aliyun cli config from '%s' failed: %s", cfgPath, string(bytes))
		return
	}

	if conf.Profiles == nil || len(conf.Profiles) == 0 {
		err = fmt.Errorf("no any configured profiles in '%s'", cfgPath)
		return
	}

	return
}

func (conf *configuration) getProfile(name string) (profile *profile, err error) {
	for _, p := range conf.Profiles {
		if p.Name == name {
			profile = p
			return
		}
	}

	err = fmt.Errorf("unable to get profile with '%s'", name)
	return
}

var oauthBaseUrlMap = map[string]string{
	"CN":   "https://oauth.aliyun.com",
	"INTL": "https://oauth.alibabacloud.com",
}

var oauthClientMap = map[string]string{
	"CN":   "4038181954557748008",
	"INTL": "4103531455503354461",
}

func (provider *CLIProfileCredentialsProvider) getCredentialsProvider(conf *configuration, profileName string) (credentialsProvider CredentialsProvider, err error) {
	p, err := conf.getProfile(profileName)
	if err != nil {
		return
	}

	switch p.Mode {
	case "AK":
		credentialsProvider, err = NewStaticAKCredentialsProviderBuilder().
			WithAccessKeyId(p.AccessKeyID).
			WithAccessKeySecret(p.AccessKeySecret).
			Build()
	case "StsToken":
		credentialsProvider, err = NewStaticSTSCredentialsProviderBuilder().
			WithAccessKeyId(p.AccessKeyID).
			WithAccessKeySecret(p.AccessKeySecret).
			WithSecurityToken(p.SecurityToken).
			Build()
	case "RamRoleArn":
		previousProvider, err1 := NewStaticAKCredentialsProviderBuilder().
			WithAccessKeyId(p.AccessKeyID).
			WithAccessKeySecret(p.AccessKeySecret).
			Build()
		if err1 != nil {
			return nil, err1
		}

		credentialsProvider, err = NewRAMRoleARNCredentialsProviderBuilder().
			WithCredentialsProvider(previousProvider).
			WithRoleArn(p.RoleArn).
			WithRoleSessionName(p.RoleSessionName).
			WithDurationSeconds(p.DurationSeconds).
			WithStsRegionId(p.StsRegion).
			WithEnableVpc(p.EnableVpc).
			WithPolicy(p.Policy).
			WithExternalId(p.ExternalId).
			Build()
	case "EcsRamRole":
		credentialsProvider, err = NewECSRAMRoleCredentialsProviderBuilder().WithRoleName(p.RoleName).Build()
	case "OIDC":
		credentialsProvider, err = NewOIDCCredentialsProviderBuilder().
			WithOIDCTokenFilePath(p.OIDCTokenFile).
			WithOIDCProviderARN(p.OIDCProviderARN).
			WithRoleArn(p.RoleArn).
			WithStsRegionId(p.StsRegion).
			WithEnableVpc(p.EnableVpc).
			WithDurationSeconds(p.DurationSeconds).
			WithRoleSessionName(p.RoleSessionName).
			WithPolicy(p.Policy).
			Build()
	case "ChainableRamRoleArn":
		previousProvider, err1 := provider.getCredentialsProvider(conf, p.SourceProfile)
		if err1 != nil {
			err = fmt.Errorf("get source profile failed: %s", err1.Error())
			return
		}
		credentialsProvider, err = NewRAMRoleARNCredentialsProviderBuilder().
			WithCredentialsProvider(previousProvider).
			WithRoleArn(p.RoleArn).
			WithRoleSessionName(p.RoleSessionName).
			WithDurationSeconds(p.DurationSeconds).
			WithStsRegionId(p.StsRegion).
			WithEnableVpc(p.EnableVpc).
			WithPolicy(p.Policy).
			WithExternalId(p.ExternalId).
			Build()
	case "CloudSSO":
		credentialsProvider, err = NewCloudSSOCredentialsProviderBuilder().
			WithSignInUrl(p.SignInUrl).
			WithAccountId(p.AccountId).
			WithAccessConfig(p.AccessConfig).
			WithAccessToken(p.AccessToken).
			WithAccessTokenExpire(p.AccessTokenExpire).
			Build()
	case "OAuth":
		siteType := strings.ToUpper(p.OauthSiteType)
		signInUrl := oauthBaseUrlMap[siteType]
		if signInUrl == "" {
			err = fmt.Errorf("invalid site type, support CN or INTL")
			return
		}
		clientId := oauthClientMap[siteType]

		credentialsProvider, err = NewOAuthCredentialsProviderBuilder().
			WithSignInUrl(signInUrl).
			WithClientId(clientId).
			WithRefreshToken(p.OauthRefreshToken).
			WithAccessToken(p.OauthAccessToken).
			WithAccessTokenExpire(p.OauthAccessTokenExpire).
			WithTokenUpdateCallback(provider.getOAuthTokenUpdateCallback()).
			Build()
	case "External":
		credentialsProvider, err = NewExternalCredentialsProviderBuilder().
			WithProcessCommand(p.ProcessCommand).
			WithCredentialUpdateCallback(provider.getExternalCredentialUpdateCallback()).
			Build()
	default:
		err = fmt.Errorf("unsupported profile mode '%s'", p.Mode)
	}

	return
}

// 默认设置为 GetHomePath，测试时便于 mock
var getHomePath = utils.GetHomePath

func (provider *CLIProfileCredentialsProvider) GetCredentials() (cc *Credentials, err error) {
	if provider.innerProvider == nil {
		cfgPath := provider.profileFile
		if cfgPath == "" {
			homeDir := getHomePath()
			if homeDir == "" {
				err = fmt.Errorf("cannot found home dir")
				return
			}

			cfgPath = path.Join(homeDir, ".aliyun/config.json")
			provider.profileFile = cfgPath
		}

		conf, err1 := newConfigurationFromPath(cfgPath)
		if err1 != nil {
			err = err1
			return
		}

		if provider.profileName == "" {
			provider.profileName = conf.Current
		}

		provider.innerProvider, err = provider.getCredentialsProvider(conf, provider.profileName)
		if err != nil {
			return
		}
	}

	innerCC, err := provider.innerProvider.GetCredentials()
	if err != nil {
		return
	}

	providerName := innerCC.ProviderName
	if providerName == "" {
		providerName = provider.innerProvider.GetProviderName()
	}

	cc = &Credentials{
		AccessKeyId:     innerCC.AccessKeyId,
		AccessKeySecret: innerCC.AccessKeySecret,
		SecurityToken:   innerCC.SecurityToken,
		ProviderName:    fmt.Sprintf("%s/%s", provider.GetProviderName(), providerName),
	}

	return
}

func (provider *CLIProfileCredentialsProvider) GetProviderName() string {
	return "cli_profile"
}

// updateOAuthTokens 更新OAuth令牌并写回配置文件
func (provider *CLIProfileCredentialsProvider) updateOAuthTokens(refreshToken, accessToken, accessKey, secret, securityToken string, accessTokenExpire, stsExpire int64) error {
	provider.fileMutex.Lock()
	defer provider.fileMutex.Unlock()

	cfgPath := provider.profileFile
	conf, err := newConfigurationFromPath(cfgPath)
	if err != nil {
		return fmt.Errorf("failed to read config file: %v", err)
	}

	profileName := provider.profileName
	profile, err := conf.getProfile(profileName)
	if err != nil {
		return fmt.Errorf("failed to get profile %s: %v", profileName, err)
	}

	// update
	profile.OauthRefreshToken = refreshToken
	profile.OauthAccessToken = accessToken
	profile.OauthAccessTokenExpire = accessTokenExpire
	profile.AccessKeyID = accessKey
	profile.AccessKeySecret = secret
	profile.SecurityToken = securityToken
	profile.StsExpire = stsExpire

	// write back with file lock
	return provider.writeConfigurationToFileWithLock(cfgPath, conf)
}

// writeConfigurationToFile 将配置写入文件，使用原子写入确保数据完整性
func (provider *CLIProfileCredentialsProvider) writeConfigurationToFile(cfgPath string, conf *configuration) error {
	// 获取原文件权限（如果存在）
	fileMode := os.FileMode(0644)
	if stat, err := os.Stat(cfgPath); err == nil {
		fileMode = stat.Mode()
	}

	// 创建唯一临时文件
	tempFile := cfgPath + ".tmp-" + strconv.FormatInt(time.Now().UnixNano(), 10)

	// 写入临时文件
	err := provider.writeConfigFile(tempFile, fileMode, conf)
	if err != nil {
		return fmt.Errorf("failed to write temp file: %v", err)
	}

	// 原子性重命名，确保文件完整性
	err = os.Rename(tempFile, cfgPath)
	if err != nil {
		// 清理临时文件
		os.Remove(tempFile)
		return fmt.Errorf("failed to rename temp file: %v", err)
	}

	return nil
}

// writeConfigFile 写入配置文件
func (provider *CLIProfileCredentialsProvider) writeConfigFile(filename string, fileMode os.FileMode, conf *configuration) error {
	f, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_RDWR, fileMode)
	if err != nil {
		return fmt.Errorf("failed to create config file: %w", err)
	}

	defer func() {
		closeErr := f.Close()
		if err == nil && closeErr != nil {
			err = fmt.Errorf("failed to close config file: %w", closeErr)
		}
	}()

	encoder := json.NewEncoder(f)
	encoder.SetIndent("", "    ")

	if err = encoder.Encode(conf); err != nil {
		return fmt.Errorf("failed to serialize config: %w", err)
	}

	return nil
}

// writeConfigurationToFileWithLock 使用操作系统级别的文件锁写入配置文件
func (provider *CLIProfileCredentialsProvider) writeConfigurationToFileWithLock(cfgPath string, conf *configuration) error {
	// 获取原文件权限（如果存在）
	fileMode := os.FileMode(0644)
	if stat, err := os.Stat(cfgPath); err == nil {
		fileMode = stat.Mode()
	}

	// 打开文件用于锁定
	file, err := os.OpenFile(cfgPath, os.O_RDWR|os.O_CREATE, fileMode)
	if err != nil {
		return fmt.Errorf("failed to open config file: %v", err)
	}

	// 获取独占锁（阻塞其他进程）
	err = lockFile(int(file.Fd()))
	if err != nil {
		file.Close()
		return fmt.Errorf("failed to acquire file lock: %v", err)
	}

	// 创建唯一临时文件
	tempFile := cfgPath + ".tmp-" + strconv.FormatInt(time.Now().UnixNano(), 10)
	err = provider.writeConfigFile(tempFile, fileMode, conf)
	if err != nil {
		unlockFile(int(file.Fd()))
		file.Close()
		return fmt.Errorf("failed to write temp file: %v", err)
	}

	// 关闭并解锁原文件，以便在Windows上可以重命名
	unlockFile(int(file.Fd()))
	file.Close()

	// 原子性重命名
	err = os.Rename(tempFile, cfgPath)
	if err != nil {
		os.Remove(tempFile)
		return fmt.Errorf("failed to rename temp file: %v", err)
	}

	return nil
}

// getOAuthTokenUpdateCallback 获取OAuth令牌更新回调函数
func (provider *CLIProfileCredentialsProvider) getOAuthTokenUpdateCallback() OAuthTokenUpdateCallback {
	return func(refreshToken, accessToken, accessKey, secret, securityToken string, accessTokenExpire, stsExpire int64) error {
		return provider.updateOAuthTokens(refreshToken, accessToken, accessKey, secret, securityToken, accessTokenExpire, stsExpire)
	}
}

// getExternalCredentialUpdateCallback 获取External凭证更新回调函数
func (provider *CLIProfileCredentialsProvider) getExternalCredentialUpdateCallback() ExternalCredentialUpdateCallback {
	return func(accessKeyId, accessKeySecret, securityToken string, expiration int64) error {
		return provider.updateExternalCredentials(accessKeyId, accessKeySecret, securityToken, expiration)
	}
}

// updateExternalCredentials 更新External凭证并写回配置文件
func (provider *CLIProfileCredentialsProvider) updateExternalCredentials(accessKeyId, accessKeySecret, securityToken string, expiration int64) error {
	provider.fileMutex.Lock()
	defer provider.fileMutex.Unlock()

	cfgPath := provider.profileFile
	conf, err := newConfigurationFromPath(cfgPath)
	if err != nil {
		return fmt.Errorf("failed to read config file: %v", err)
	}

	profileName := provider.profileName
	profile, err := conf.getProfile(profileName)
	if err != nil {
		return fmt.Errorf("failed to get profile %s: %v", profileName, err)
	}

	// update
	profile.AccessKeyID = accessKeyId
	profile.AccessKeySecret = accessKeySecret
	profile.SecurityToken = securityToken
	if expiration > 0 {
		profile.StsExpire = expiration
	}

	// write back with file lock
	return provider.writeConfigurationToFileWithLock(cfgPath, conf)
}
