package utilities

import (
	"context"
	"fmt"
	"strconv"

	"github.com/hashicorp/terraform-plugin-framework/attr"
	"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/bservice"
	"repository.basistech.ru/BASIS/terraform-provider-dynamix/internal/client"
	"repository.basistech.ru/BASIS/terraform-provider-dynamix/internal/service/cloudapi/bservice/models"
	"repository.basistech.ru/BASIS/terraform-provider-dynamix/internal/service/cloudapi/ic"
	"repository.basistech.ru/BASIS/terraform-provider-dynamix/internal/status"
)

// BServiceResourceCheckPresence checks if BService with serviceID exists
func BServiceResourceCheckPresence(ctx context.Context, serviceID uint64, c *client.Client) (*bservice.RecordBasicService, error) {
	tflog.Info(ctx, fmt.Sprintf("BServiceResourceCheckPresence: Get info about service with ID - %v", serviceID))

	record, err := c.CloudAPI().BService().Get(ctx, bservice.GetRequest{ServiceID: serviceID})
	if err != nil {
		return nil, fmt.Errorf("BServiceResourceCheckPresence: cannot get info about resource with error: %w", err)
	}

	tflog.Info(ctx, "BServiceResourceCheckPresence: response from CloudAPI().BService().Get", map[string]any{"service_id": serviceID, "response": record})

	return record, err
}

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

	diags := diag.Diagnostics{}

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

	record, err := BServiceResourceCheckPresence(ctx, resourceId, c)
	if err != nil {
		diags.AddError("BSerivceReadStatus: Unable to Read bservice before status check", err.Error())
		return diags
	}

	// check resource status
	switch record.Status {
	case status.Disabled:
		tflog.Info(ctx, "The BService is in status Disabled, troubles may occur with update. Please, enable BService first.")
	case status.Modeled:
		diags.AddError("The basic service is in status: %s, please, contact support for more information", record.Status)
	case status.Disabling:
		tflog.Info(ctx, fmt.Sprintf("The basic service is in status: %s, troubles can occur with the update.", record.Status))
	case status.Deleted:
		restore := state.Restore.ValueBool()
		if state.Restore.IsNull() {
			restore = true
		} // default true
		if restore {
			// attempt to restore bservice
			tflog.Info(ctx, "BServiceReadStatus: BService with status.Deleted is being read, attempt to restore it", map[string]any{
				"BService": state.ID.ValueString(),
				"status":   record.Status})
			diags.Append(restoreBservice(ctx, resourceId, c)...)
			if diags.HasError() {
				tflog.Error(ctx, "BServiceReadStatus: cannot restore BService")
				return diags
			}
			tflog.Info(ctx, "BServiceReadStatus: BService restored successfully", map[string]any{"service_id": resourceId})
		} else {
			tflog.Info(ctx, "BServiceReadStatus: BService is i status Deleted but restore is not specified")
		}

	case status.Destroyed:
		diags.AddError(
			"BSerivceReadStatus: BService is in status Destroyed",
			fmt.Sprintf("the resource with bservice_id %d cannot be read or updated because it has been destroyed", resourceId),
		)
		return diags

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

	return nil
}

func BServiceResourceCreate(ctx context.Context, plan *models.RecordBasicServiceResourceModel, c *client.Client) (*uint64, diag.Diagnostics) {
	tflog.Info(ctx, "Start BServiceResourceCreate", map[string]any{"service_id": plan.ServiceId.ValueInt64()})

	diags := diag.Diagnostics{}

	err := ic.ExistRG(ctx, uint64(plan.RGID.ValueInt64()), c)
	if err != nil {
		diags.AddError(fmt.Sprintf("resourceBasicServiceCreate: can't create basic service because RGID %d is not allowed or does not exist", plan.RGID.ValueInt64()), err.Error())
		return nil, diags
	}

	req := bservice.CreateRequest{}

	req.Name = plan.Name.ValueString()
	req.RGID = uint64(plan.RGID.ValueInt64())

	if !plan.SSHKey.IsNull() {
		req.SSHKey = plan.SSHKey.ValueString()
	}

	if !plan.SSHUser.IsNull() {
		req.SSHUser = plan.SSHUser.ValueString()
	}

	// Make request and get response
	serviceId, err := c.CloudAPI().BService().Create(ctx, req)
	if err != nil {
		tflog.Error(ctx, "Error response for create bservice", map[string]any{"error": err.Error()})
		diags.AddError("Unable to Create bservice", err.Error())
		return nil, diags
	}

	plan.ID = types.StringValue(strconv.Itoa(int(serviceId)))

	enable := plan.Enable.ValueBool()
	if enable {
		tflog.Info(ctx, "resourceBasicServiceCreate: before calling CloudAPI().BService().Enable", map[string]any{"service_id": serviceId})
		res, err := c.CloudAPI().BService().Enable(ctx, bservice.EnableRequest{ServiceID: serviceId})
		if err != nil {
			diags.AddWarning(
				"resourceBasicServiceCreate: cannot enable BService",
				err.Error(),
			)
			return &serviceId, diags
		}
		tflog.Info(ctx, "resourceBasicServiceCreate: response from CloudAPI().BService().Enable", map[string]any{"service_id": serviceId, "response": res})
	}

	if plan.Start.ValueBool() {
		if !enable {
			diags.AddWarning(
				"can not start bservice that is not enabled. Set enable = true and start = true to enable and start bservice",
				fmt.Sprintf("service_id: %v", serviceId),
			)
			return &serviceId, diags
		}
		tflog.Info(ctx, "resourceBasicServiceCreate: before calling CloudAPI().BService().Start", map[string]any{"service_id": serviceId})
		_, err := c.CloudAPI().BService().Start(ctx, bservice.StartRequest{
			ServiceID: serviceId,
		})
		if err != nil {
			diags.AddWarning(
				"resourceBasicServiceCreate: cannot start BService",
				err.Error(),
			)
			return &serviceId, diags
		}
	}
	tflog.Info(ctx, "End resourceBasicServiceCreate", map[string]any{"service_id": serviceId})
	return &serviceId, diags
}

// EnableDisableBService performs BService Enable/Disable request.
// Returns error in case of failures.
func EnableDisableBService(ctx context.Context, plan *models.RecordBasicServiceResourceModel, c *client.Client) diag.Diagnostics {
	tflog.Info(ctx, "Start EnableDisableBService", map[string]any{"service_id": plan.ID.ValueString()})

	diags := diag.Diagnostics{}

	serviceID, err := strconv.Atoi(plan.ID.ValueString())
	if err != nil {
		diags.AddError("EnableDisableBService: Cannot parse ID from state", err.Error())
		return diags
	}

	if plan.Enable.ValueBool() {
		tflog.Info(ctx, "EnableDisableBService: before calling CloudAPI().BService().Enable", map[string]any{"service_id": serviceID})
		res, err := c.CloudAPI().BService().Enable(ctx, bservice.EnableRequest{ServiceID: uint64(serviceID)})
		if err != nil {
			diags.AddError(
				"EnableDisableBService: cannot enable BService",
				err.Error(),
			)
			return diags
		}
		tflog.Info(ctx, "EnableDisableBService: response from CloudAPI().BService().Enable", map[string]any{"service_id": serviceID, "response": res})
		return nil
	} else {
		tflog.Info(ctx, "EnableDisableBService: before calling CloudAPI().BService().Disable", map[string]any{"service_id": serviceID})
		res, err := c.CloudAPI().BService().Disable(ctx, bservice.DisableRequest{ServiceID: uint64(serviceID)})
		if err != nil {
			diags.AddError(
				"EnableDisableBService: cannot disable BService",
				err.Error(),
			)
			return diags
		}
		tflog.Info(ctx, "EnableDisableBService: response from CloudAPI().BService().Disable", map[string]any{"service_id": serviceID, "response": res})
	}

	return nil
}

// StartStopBService performs BService Start/Stop request.
// Returns error in case of failures.
func StartStopBService(ctx context.Context, plan *models.RecordBasicServiceResourceModel, c *client.Client) diag.Diagnostics {
	tflog.Info(ctx, "Start StartStopBService", map[string]any{"service_id": plan.ID.ValueString()})

	diags := diag.Diagnostics{}

	serviceID, err := strconv.Atoi(plan.ID.ValueString())
	if err != nil {
		diags.AddError("StartStopBService: Cannot parse ID from state", err.Error())
		return diags
	}

	if plan.Start.ValueBool() {
		tflog.Info(ctx, "StartStopBService: before calling CloudAPI().BService().Start", map[string]any{"service_id": serviceID})
		res, err := c.CloudAPI().BService().Start(ctx, bservice.StartRequest{ServiceID: uint64(serviceID)})
		if err != nil {
			diags.AddError(
				"StartStopBService: cannot start BService",
				err.Error(),
			)
			return diags
		}
		tflog.Info(ctx, "StartStopBService: response from CloudAPI().BService().Start", map[string]any{"service_id": serviceID, "response": res})
		return nil
	} else {
		tflog.Info(ctx, "StartStopBService: before calling CloudAPI().BService().Stop", map[string]any{"service_id": serviceID})
		res, err := c.CloudAPI().BService().Stop(ctx, bservice.StopRequest{ServiceID: uint64(serviceID)})
		if err != nil {
			diags.AddError(
				"StartStopBService: cannot stop BService",
				err.Error(),
			)
			return diags
		}
		tflog.Info(ctx, "StartStopBService: response from CloudAPI().BService().Stop", map[string]any{"service_id": serviceID, "response": res})
	}

	return nil
}

func SnapshotsBService(ctx context.Context, oldSnapshots basetypes.SetValue, newSnapshots basetypes.SetValue, serviceID uint64, c *client.Client) diag.Diagnostics {
	diags := diag.Diagnostics{}
	// Handle snapshot changes in the plan
	tflog.Info(ctx, "Start SnapshotsBService", map[string]any{"service_id": serviceID})

	addMap, rollbackMap, deleteMap := differenceSnapshots(oldSnapshots, newSnapshots)

	tflog.Debug(ctx, "SnapshotsBService: Snapshots to be deleted", map[string]any{"deleted_snapshots": deleteMap})
	tflog.Debug(ctx, "SnapshotsBService: Snapshots to be added", map[string]any{"added_snapshots": addMap})
	tflog.Debug(ctx, "SnapshotsBService: Snapshots to be updated", map[string]any{"updated_snapshots": rollbackMap})

	if len(deleteMap) > 0 {
		for _, snapshot := range deleteMap {
			req := bservice.SnapshotDeleteRequest{
				ServiceID: serviceID,
				Label:     snapshot["label"].(types.String).ValueString(),
			}
			tflog.Info(ctx, "SnapshotsBService: before calling CloudAPI().BService().SnapshotDelete", map[string]any{"service_id": serviceID, "req": req})
			_, err := c.CloudAPI().BService().SnapshotDelete(ctx, req)
			if err != nil {
				tflog.Error(ctx, "SnapshotsBService: Failed to delete snapshot")
				diags.AddError(fmt.Sprintf("SnapshotsBService: failed to delete snapshot %d from service %d",
					snapshot["label"].(types.String).ValueString(), serviceID), err.Error())
				return diags
			}
			tflog.Info(ctx, "Deleted snapshot", map[string]any{"service_id": serviceID, "label": req.Label})
		}
	}

	if len(addMap) > 0 {
		for _, snapshot := range addMap {
			req := bservice.SnapshotCreateRequest{
				ServiceID: serviceID,
				Label:     snapshot["label"].(types.String).ValueString(),
			}

			tflog.Info(ctx, "SnapshotsBService: before calling CloudAPI().BService().SnapshotCreate", map[string]any{"service_id": serviceID, "req": req})
			_, err := c.CloudAPI().BService().SnapshotCreate(ctx, req)
			if err != nil {
				tflog.Error(ctx, "SnapshotsBService: Failed to create snapshot")
				diags.AddError(fmt.Sprintf("SnapshotsBService: failed to create snapshot %d for service %d",
					snapshot["label"].(types.String).ValueString(), serviceID), err.Error())
				return diags
			}
			tflog.Info(ctx, "Created snapshot", map[string]any{"service_id": serviceID, "label": req.Label})
		}
	}

	if len(rollbackMap) > 0 {
		for _, snapshot := range rollbackMap {
			if snapshot["rollback"].(types.Bool).ValueBool() {
				req := bservice.SnapshotRollbackRequest{
					ServiceID: serviceID,
					Label:     snapshot["label"].(types.String).ValueString(),
				}
				tflog.Info(ctx, "SnapshotsBService: before calling CloudAPI().BService().SnapshotRollback", map[string]any{"service_id": serviceID, "req": req})
				_, err := c.CloudAPI().BService().SnapshotRollback(ctx, req)
				if err != nil {
					tflog.Error(ctx, "SnapshotsBService: Failed to rollback snapshot")
					diags.AddError(fmt.Sprintf("SnapshotsBService: failed to rollback snapshot %d from service %d",
						snapshot["label"].(types.String).ValueString(), serviceID), err.Error())
					return diags
				}
				tflog.Info(ctx, "Rolled back snapshot", map[string]any{"service_id": serviceID, "label": req.Label})
			}
		}
	}
	return nil

}

func differenceSnapshots(oldSet, newSet types.Set) (added, rollback, removed []map[string]attr.Value) {
	oldSlice := oldSet.Elements()
	newSlice := newSet.Elements()

	added = make([]map[string]attr.Value, 0)
	rollback = make([]map[string]attr.Value, 0)
	removed = make([]map[string]attr.Value, 0)

	for _, oldSnapshot := range oldSlice {
		oldMap := oldSnapshot.(types.Object).Attributes()
		found := false
		for _, newSnapshot := range newSlice {
			newMap := newSnapshot.(types.Object).Attributes()
			if newMap["label"] == oldMap["label"] {
				if newMap["rollback"] != oldMap["rollback"] {
					rollback = append(rollback, newMap)
				}
				found = true
				break
			}
		}
		if found {
			continue
		}
		removed = append(removed, oldMap)
	}

	for _, newSnapshot := range newSlice {
		newMap := newSnapshot.(types.Object).Attributes()
		found := false
		for _, oldSnapshot := range oldSlice {
			oldMap := oldSnapshot.(types.Object).Attributes()
			if newMap["label"] == oldMap["label"] {
				if newMap["rollback"] != oldMap["rollback"] {
					rollback = append(rollback, newMap)
				}
				found = true
				break
			}
		}
		if found {
			continue
		}
		added = append(added, newMap)
	}

	return
}

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

	restoreReq := bservice.RestoreRequest{
		ServiceID: serviceID,
	}

	tflog.Info(ctx, "restoreBservice: before calling CloudAPI().BService().Restore", map[string]any{"service_id": serviceID, "req": restoreReq})

	res, err := c.CloudAPI().BService().Restore(ctx, restoreReq)
	if err != nil {
		diags.AddError(
			"restoreBservice: cannot restore BService",
			err.Error(),
		)
		return diags
	}
	tflog.Info(ctx, "restoreBservice: response from CloudAPI().BService().Restore", map[string]any{"service_id": serviceID, "response": res})

	return nil
}
