package utilities

import (
	"context"
	"fmt"
	"strconv"
	"time"

	"github.com/hashicorp/terraform-plugin-framework/diag"
	"github.com/hashicorp/terraform-plugin-framework/types"
	"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
	"github.com/hashicorp/terraform-plugin-log/tflog"
	"repository.basistech.ru/BASIS/decort-golang-sdk/pkg/cloudapi/account"
	"repository.basistech.ru/BASIS/terraform-provider-dynamix/internal/client"
	"repository.basistech.ru/BASIS/terraform-provider-dynamix/internal/service/cloudapi/account/models"
	"repository.basistech.ru/BASIS/terraform-provider-dynamix/internal/status"
)

// AccountResourceCheckPresence checks if account with accountId exists
func AccountResourceCheckPresence(ctx context.Context, accountId uint64, c *client.Client) (*account.RecordAccount, error) {
	tflog.Info(ctx, fmt.Sprintf("AccountResourceCheckPresence: Get info about resource with ID - %v", accountId))

	accountRecord, err := c.CloudAPI().Account().Get(ctx, account.GetRequest{AccountID: accountId})
	if err != nil {
		return nil, fmt.Errorf("AccountResourceCheckPresence: cannot get info about resource with error: %w", err)
	}

	tflog.Info(ctx, "AccountResourceCheckPresence: response from CloudAPI().Account().Get", map[string]any{"account_id": accountId, "response": accountRecord})

	return accountRecord, err
}

// AccountReadStatus loads account resource by its id, gets it current status. Performs restore and enable if needed for
// Deleted status.
// In case of failure returns errors.
func AccountReadStatus(ctx context.Context, state *models.ResourceAccountModel, c *client.Client) diag.Diagnostics {
	tflog.Info(ctx, "AccountReadStatus: Read status resource with ID", map[string]any{"account_id": state.Id.ValueString()})

	diags := diag.Diagnostics{}

	accountId, err := strconv.ParseUint(state.Id.ValueString(), 10, 64)
	if err != nil {
		diags.AddError("AccountReadStatus: Cannot parse resource ID from state", err.Error())
		return diags
	}

	recordAccount, err := AccountResourceCheckPresence(ctx, accountId, c)
	if err != nil {
		diags.AddError("AccountReadStatus: Unable to Read account before status check", err.Error())
		return diags
	}

	// check resource status
	switch recordAccount.Status {
	case status.Disabled:
		tflog.Info(ctx, "The account is in status Disabled, troubles may occur with update. Please, enable account first.")

	case status.Deleted:
		restore := state.Restore.ValueBool()
		if state.Restore.IsNull() {
			restore = true
		} // default true
		if restore {
			// attempt to restore account
			tflog.Info(ctx, "AccountReadStatus: account with status.Deleted is being read, attempt to restore it", map[string]any{
				"account_id": accountId,
				"status":     recordAccount.Status})
			diags.Append(RestoreAccount(ctx, accountId, c)...)
			if diags.HasError() {
				tflog.Error(ctx, "AccountReadStatus: cannot restore account")
				return diags
			}
			tflog.Info(ctx, "AccountReadStatus: account restored successfully", map[string]any{"account_id": accountId})
			state.LastUpdated = types.StringValue(time.Now().Format(time.RFC850))
		} else {
			tflog.Info(ctx, "AccountReadStatus: account is i status Deleted but restore is not specified")
		}

	case status.Destroyed:
		diags.AddError(
			"AccountReadStatus: Account is in status Destroyed",
			fmt.Sprintf("the resource with account_id %d cannot be read or updated because it has been destroyed", accountId),
		)
		return diags

	case status.Destroying:
		diags.AddError(
			"AccountReadStatus: Account is in progress with status Destroying",
			fmt.Sprintf("the resource with account_id %d cannot be read or updated because it is currently being destroyed", accountId),
		)
		return diags
	}

	return nil
}

// RestoreAccount performs account Restore request.
// Returns error in case of failures.
func RestoreAccount(ctx context.Context, accountId uint64, c *client.Client) diag.Diagnostics {
	diags := diag.Diagnostics{}

	restoreReq := account.RestoreRequest{
		AccountID: accountId,
	}

	tflog.Info(ctx, "RestoreAccount: before calling CloudAPI().Account().Restore", map[string]any{"account_id": accountId, "req": restoreReq})

	res, err := c.CloudAPI().Account().Restore(ctx, restoreReq)
	if err != nil {
		diags.AddError(
			"RestoreAccount: cannot restore account",
			err.Error(),
		)
		return diags
	}
	tflog.Info(ctx, "RestoreAccount: response from CloudAPI().Account().Restore", map[string]any{"account_id": accountId, "response": res})

	return nil
}

// EnableDisableAccount performs account Enable/Disable request.
// Returns error in case of failures.
func EnableDisableAccount(ctx context.Context, accountId uint64, plan *models.ResourceAccountModel, c *client.Client) diag.Diagnostics {
	tflog.Info(ctx, "Start EnableDisableAccount", map[string]any{"account_id": accountId})

	diags := diag.Diagnostics{}
	enable := plan.Enable.ValueBool()

	if enable {
		tflog.Info(ctx, "EnableDisableAccount: before calling CloudAPI().Account().Enable", map[string]any{"account_id": accountId})
		res, err := c.CloudAPI().Account().Enable(ctx, account.DisableEnableRequest{AccountID: accountId})
		if err != nil {
			diags.AddError(
				"EnableDisableAccount: cannot enable account",
				err.Error(),
			)
			return diags
		}
		tflog.Info(ctx, "EnableDisableAccount: response from CloudAPI().Account().Enable", map[string]any{"account_id": accountId, "response": res})

		return nil
	}

	tflog.Info(ctx, "EnableDisableAccount: before calling CloudAPI().Account().Disable", map[string]any{"account_id": accountId})
	reason := plan.Reason.ValueString()
	res, err := c.CloudAPI().Account().Disable(ctx, account.DisableEnableRequest{AccountID: accountId, Reason: reason})
	if err != nil {
		diags.AddError(
			"EnableDisableAccount: cannot disable account",
			err.Error(),
		)
		return diags
	}
	tflog.Info(ctx, "EnableDisableAccount: response from CloudAPI().Account().Disable", map[string]any{"account_id": accountId, "response": res})

	return nil
}

// UpdateAccount updates disk data: account_name, resource_limits, send_access_emails.
// Returns error in case of failures.
func UpdateAccount(ctx context.Context, accountId uint64, plan, state *models.ResourceAccountModel, c *client.Client) diag.Diagnostics {
	tflog.Info(ctx, "Start UpdateAccount", map[string]any{"account_id": accountId})

	var diags diag.Diagnostics
	var updateNeeded bool

	updateReq := account.UpdateRequest{
		AccountID: accountId,
	}

	// check if account_name was changed
	if !plan.AccountName.Equal(state.AccountName) {
		updateReq.Name = plan.AccountName.ValueString()
		updateNeeded = true
	}

	// check if description was changed
	if !plan.Desc.Equal(state.Desc) {
		updateReq.Description = plan.Desc.ValueString()
		updateNeeded = true
	}

	// check if resource_limits were changed
	if !plan.ResourceLimits.Equal(state.ResourceLimits) && !plan.ResourceLimits.IsUnknown() {
		tflog.Info(ctx, "UpdateAccount: new ResourceLimits specified", map[string]any{"account_id": accountId})
		var resourceLimitsPlan models.ResourceLimitsInAccountResourceModel
		diags.Append(plan.ResourceLimits.As(ctx, &resourceLimitsPlan, basetypes.ObjectAsOptions{})...)
		if diags.HasError() {
			tflog.Error(ctx, "UpdateAccount: cannot populate ResourceLimits with plan.ResourceLimits object element")
			return diags
		}

		if resourceLimitsPlan.CUM.ValueFloat64() == 0 {
			updateReq.MaxMemoryCapacity = -1
		} else {
			updateReq.MaxMemoryCapacity = int64(resourceLimitsPlan.CUM.ValueFloat64())
		}

		if resourceLimitsPlan.CUD.ValueFloat64() == 0 {
			updateReq.MaxVDiskCapacity = -1
		} else {
			updateReq.MaxVDiskCapacity = int64(resourceLimitsPlan.CUD.ValueFloat64())
		}

		if resourceLimitsPlan.CUC.ValueFloat64() == 0 {
			updateReq.MaxCPUCapacity = -1
		} else {
			updateReq.MaxCPUCapacity = int64(resourceLimitsPlan.CUC.ValueFloat64())
		}

		if resourceLimitsPlan.CUI.ValueFloat64() == 0 {
			updateReq.MaxNumPublicIP = -1
		} else {
			updateReq.MaxNumPublicIP = int64(resourceLimitsPlan.CUI.ValueFloat64())
		}

		if resourceLimitsPlan.CUNP.ValueFloat64() == 0 {
			updateReq.MaxNetworkPeerTransfer = -1
		} else {
			updateReq.MaxNetworkPeerTransfer = int64(resourceLimitsPlan.CUNP.ValueFloat64())
		}

		if resourceLimitsPlan.GPUUnits.ValueFloat64() == 0 {
			updateReq.GPUUnits = -1
		} else {
			updateReq.GPUUnits = int64(resourceLimitsPlan.GPUUnits.ValueFloat64())
		}

		updateNeeded = true
	}

	// check if send_access_emails was changed
	if !plan.SendAccessEmails.Equal(state.SendAccessEmails) && !plan.SendAccessEmails.IsNull() {
		updateReq.SendAccessEmails = plan.SendAccessEmails.ValueBool()
		updateNeeded = true
	}

	if !updateNeeded {
		tflog.Info(ctx, "UpdateAccount: no general account update is needed because neither account_name, nor resource_limits, nor send_access_emails were changed.", map[string]any{
			"account_id": plan.Id.ValueString(),
		})
		return nil
	}

	// perform account update
	tflog.Info(ctx, "UpdateAccount: before calling CloudAPI().Account().Update", map[string]any{
		"account_id": accountId,
		"req":        updateReq,
	})
	res, err := c.CloudAPI().Account().Update(ctx, updateReq)
	if err != nil {
		diags.AddError("UpdateAccount: Unable to update account",
			err.Error())
		return diags
	}
	tflog.Info(ctx, "UpdateAccount: response from CloudAPI().Account().Update", map[string]any{
		"account_id": accountId,
		"response":   res})

	return nil
}

// AddDeleteUsersAccount adds/deletes users to/from account.
// In case of failure returns errors.
func AddDeleteUsersAccount(ctx context.Context, accountId uint64, plan, state *models.ResourceAccountModel, c *client.Client) diag.Diagnostics {
	tflog.Info(ctx, "Start AddDeleteUsersAccount: new users specified", map[string]any{"account_id": accountId})
	diags := diag.Diagnostics{}

	usersPlan := make([]models.UsersModel, 0, len(plan.Users.Elements()))
	diags.Append(plan.Users.ElementsAs(ctx, &usersPlan, true)...)
	if diags.HasError() {
		tflog.Error(ctx, "AddDeleteUsersAccount: cannot populate usersPlan with plan.Users list elements")
		return diags
	}

	usersState := make([]models.UsersModel, 0, len(state.Users.Elements()))
	diags.Append(state.Users.ElementsAs(ctx, &usersState, true)...)
	if diags.HasError() {
		tflog.Error(ctx, "AddDeleteUsersAccount: cannot populate usersState with state.Users list elements")
		return diags
	}

	// define users to be deleted, added and updated
	var deletedUsers, addedUsers, updatedUsers []models.UsersModel
	for _, user := range usersState {
		if !containsUser(usersPlan, user) {
			deletedUsers = append(deletedUsers, user)
		}
	}
	for _, user := range usersPlan {
		if !containsUser(usersState, user) {
			addedUsers = append(addedUsers, user)
		} else if isChangedUser(usersState, user) {
			updatedUsers = append(updatedUsers, user)
		}
	}

	// delete users
	if len(deletedUsers) == 0 {
		tflog.Info(ctx, "AddDeleteUsersAccount: no users need to be deleted", map[string]any{"account_id": accountId})
	}
	if len(deletedUsers) > 0 {
		tflog.Info(ctx, "AddDeleteUsersAccount: users need to be deleted", map[string]any{
			"accountId":    accountId,
			"deletedUsers": deletedUsers})

		for _, user := range deletedUsers {
			delUserReq := account.DeleteUserRequest{
				AccountID: accountId,
				UserID:    user.UserID.ValueString(),
			}

			tflog.Info(ctx, "AddDeleteUsersAccount: before calling CloudAPI().Account().DeleteUser", map[string]any{"account_id": accountId, "req": delUserReq})
			res, err := c.CloudAPI().Account().DeleteUser(ctx, delUserReq)
			tflog.Info(ctx, "AddDeleteUsersAccount: response from CloudAPI().Account().DeleteUser", map[string]any{"account_id": accountId, "response": res})
			if err != nil {
				diags.AddError(
					"AddDeleteUsersAccount: can not delete user from account",
					err.Error())
			}
		}
	}

	// add users
	if len(addedUsers) == 0 {
		tflog.Info(ctx, "AddDeleteUsersAccount: no users needs to be added", map[string]any{"account_id": accountId})
	}
	if len(addedUsers) > 0 {
		tflog.Info(ctx, "AddDeleteUsersAccount: users need to be added", map[string]any{"account_id": accountId})

		for _, user := range addedUsers {
			addUserReq := account.AddUserRequest{
				AccountID:  accountId,
				UserID:     user.UserID.ValueString(),
				AccessType: user.AccessType.ValueString(),
			}

			tflog.Info(ctx, "AddDeleteUsersAccount: before calling CloudAPI().Account().AddUser", map[string]any{
				"account_id": accountId,
				"addUserReq": addUserReq})
			res, err := c.CloudAPI().Account().AddUser(ctx, addUserReq)
			if err != nil {
				diags.AddError("AddDeleteUsersAccount: Unable to add users to account",
					err.Error())
			}
			tflog.Info(ctx, "AddDeleteUsersAccount: response from CloudAPI().Account().AddUser", map[string]any{
				"account_id": accountId,
				"response":   res})
		}
	}

	// update users
	if len(updatedUsers) == 0 {
		tflog.Info(ctx, "AddDeleteUsersAccount: no users needs to be updated", map[string]any{"account_id": accountId})
	}
	if len(updatedUsers) > 0 {
		tflog.Info(ctx, "AddDeleteUsersAccount: users need to be updated", map[string]any{"account_id": accountId})

		for _, user := range updatedUsers {
			updUserReq := account.UpdateUserRequest{
				AccountID:  accountId,
				UserID:     user.UserID.ValueString(),
				AccessType: user.AccessType.ValueString(),
			}

			tflog.Info(ctx, "AddDeleteUsersAccount: before calling CloudAPI().Account().UpdateUser", map[string]any{
				"account_id":   accountId,
				"updatedUsers": updatedUsers})
			res, err := c.CloudAPI().Account().UpdateUser(ctx, updUserReq)
			if err != nil {
				diags.AddError("AddDeleteUsersAccount: Unable to update users",
					err.Error())
			}
			tflog.Info(ctx, "AddDeleteUsersAccount: response from CloudAPI().Account().UpdateUser", map[string]any{
				"account_id": accountId,
				"response":   res})
		}
	}

	return diags
}

func containsUser(users []models.UsersModel, target models.UsersModel) bool {
	for _, user := range users {
		if target.UserID == user.UserID {
			return true
		}
	}
	return false
}

func isChangedUser(users []models.UsersModel, target models.UsersModel) bool {
	for _, user := range users {
		if user.UserID.Equal(target.UserID) && !user.AccessType.Equal(target.AccessType) {
			return true
		}
	}
	return false
}
