/*
Copyright (c) 2019-2022 Digital Energy Cloud Solutions LLC. All Rights Reserved.
Authors:
Petr Krutov, <petr.krutov@digitalenergy.online>
Stanislav Solovev, <spsolovev@digitalenergy.online>
Kasim Baybikov, <kmbaybikov@basistech.ru>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

/*
Terraform DECORT provider - manage resources provided by DECORT (Digital Energy Cloud
Orchestration Technology) with Terraform by Hashicorp.

Source code: https://repository.basistech.ru/BASIS/terraform-provider-decort

Please see README.md to learn where to place source code so that it
builds seamlessly.

Documentation: https://repository.basistech.ru/BASIS/terraform-provider-decort/wiki
*/

package bservice

import (
	"context"
	"errors"
	"strconv"

	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
	log "github.com/sirupsen/logrus"
	"repository.basistech.ru/BASIS/decort-golang-sdk/pkg/cloudapi/bservice"
	"repository.basistech.ru/BASIS/terraform-provider-decort/internal/constants"
	"repository.basistech.ru/BASIS/terraform-provider-decort/internal/controller"
	"repository.basistech.ru/BASIS/terraform-provider-decort/internal/dc"
	"repository.basistech.ru/BASIS/terraform-provider-decort/internal/status"
)

func resourceBasicServiceCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
	log.Debugf("resourceBasicServiceCreate")
	c := m.(*controller.ControllerCfg)
	req := bservice.CreateRequest{}

	haveRGID, err := existRGID(ctx, d, m)
	if err != nil {
		return diag.FromErr(err)
	}

	if !haveRGID {
		return diag.Errorf("resourceBasicServiceCreate: can't create basic service because RGID %d is not allowed or does not exist", d.Get("rg_id").(int))
	}

	req.Name = d.Get("service_name").(string)
	req.RGID = uint64(d.Get("rg_id").(int))

	if zoneID, ok := d.GetOk("zone_id"); ok {
		req.ZoneID = uint64(zoneID.(int))
	}
	if sshKey, ok := d.GetOk("ssh_key"); ok {
		req.SSHKey = sshKey.(string)
	}
	if sshUser, ok := d.GetOk("ssh_user"); ok {
		req.SSHUser = sshUser.(string)
	}

	serviceId, err := c.CloudAPI().BService().Create(ctx, req)
	if err != nil {
		return diag.FromErr(err)
	}

	d.SetId(strconv.FormatUint(serviceId, 10))
	d.Set("service_id", serviceId)

	service, err := utilityBasicServiceCheckPresence(ctx, d, m)
	if err != nil {
		d.SetId("")
		return diag.FromErr(err)
	}

	warnings := dc.Warnings{}
	enable := d.Get("enable").(bool)

	if enable && (service.Status == status.Disabled || service.Status == status.Created) {
		log.Debugf("trying to enable bservice %v", serviceId)
		_, err := c.CloudAPI().BService().Enable(ctx, bservice.EnableRequest{
			ServiceID: serviceId,
		})

		if err != nil {
			warnings.Add(err)
		}
	}
	if d.Get("start").(bool) {
		log.Debugf("trying to start bservice %v", serviceId)

		if !enable {
			warnings.Add(errors.New("can not start bservice that is not enabled. Set enable = true and start = true to enable and start bservice"))
		}

		if enable {
			_, err := c.CloudAPI().BService().Start(ctx, bservice.StartRequest{
				ServiceID: serviceId,
			})

			if err != nil {
				warnings.Add(err)
			}
		}
	}

	return append(warnings.Get(), resourceBasicServiceRead(ctx, d, m)...)
}

func resourceBasicServiceRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
	log.Debugf("resourceBasicServiceRead")

	// c := m.(*controller.ControllerCfg)

	bs, err := utilityBasicServiceCheckPresence(ctx, d, m)
	if err != nil {
		d.SetId("")
		return diag.FromErr(err)
	}

	hasChanged := false

	switch bs.Status {
	case status.Modeled:
		return diag.Errorf("The basic service is in status: %s, please, contact support for more information", bs.Status)
	case status.Created:
	case status.Enabled:
	case status.Enabling:
	case status.Disabled:
		log.Debugf("The basic service is in status: %s, troubles can occur with the update. Please, enable bservice first.", bs.Status)
	case status.Disabling:
		log.Debugf("The basic service is in status: %s, troubles can occur with the update.", bs.Status)
	case status.Deleted:
		// id, _ := strconv.ParseUint(d.Id(), 10, 64)
		// restoreReq := bservice.RestoreRequest{
		// 	ServiceID: id,
		// }
		// enableReq := bservice.EnableRequest{
		// 	ServiceID: id,
		// }

		// _, err := c.CloudAPI().BService().Restore(ctx, restoreReq)
		// if err != nil {
		// 	return diag.FromErr(err)
		// }

		// _, err = c.CloudAPI().BService().Enable(ctx, enableReq)
		// if err != nil {
		// 	return diag.FromErr(err)
		// }

		// hasChanged = true
	case status.Deleting:
	case status.Destroyed:
		d.SetId("")
		return diag.Errorf("The resource cannot be updated because it has been destroyed")
		// return resourceBasicServiceCreate(ctx, d, m)
	case status.Destroying:
		return diag.Errorf("The basic service is in progress with status: %s", bs.Status)
	case status.Restoring:
	case status.Reconfiguring:
	}

	if hasChanged {
		bs, err = utilityBasicServiceCheckPresence(ctx, d, m)
		if err != nil {
			d.SetId("")
			return diag.FromErr(err)
		}
	}

	flattenService(d, bs)

	return nil
}

func resourceBasicServiceDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
	log.Debugf("resourceBasicServiceDelete")
	c := m.(*controller.ControllerCfg)

	_, err := utilityBasicServiceCheckPresence(ctx, d, m)
	if err != nil {
		return diag.FromErr(err)
	}

	req := bservice.DeleteRequest{
		ServiceID:   uint64(d.Get("service_id").(int)),
		Permanently: d.Get("permanently").(bool),
	}

	_, err = c.CloudAPI().BService().Delete(ctx, req)
	if err != nil {
		return diag.FromErr(err)
	}

	d.SetId("")

	return nil
}

func resourceBasicServiceUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
	log.Debugf("resourceBasicServiceUpdate")
	c := m.(*controller.ControllerCfg)

	haveRGID, err := existRGID(ctx, d, m)
	if err != nil {
		return diag.FromErr(err)
	}

	if !haveRGID {
		return diag.Errorf("resourceBasicServiceUpdate: can't create basic service because RGID %d is not allowed or does not exist", d.Get("rg_id").(int))
	}

	bs, err := utilityBasicServiceCheckPresence(ctx, d, m)
	if err != nil {
		d.SetId("")
		return diag.FromErr(err)
	}

	hasChanged := false

	switch bs.Status {
	case status.Modeled:
		return diag.Errorf("The basic service is in status: %s, please, contact support for more information", bs.Status)
	case status.Created:
	case status.Enabled:
	case status.Enabling:
	case status.Disabled:
		log.Debugf("The basic service is in status: %s, troubles can occur with the update. Please, enable bservice first.", bs.Status)
	case status.Disabling:
		log.Debugf("The basic service is in status: %s, troubles can occur with the update.", bs.Status)
	case status.Deleted:
		if d.Get("restore").(bool) {
			restoreReq := bservice.RestoreRequest{
				ServiceID: bs.ID,
			}
			_, err := c.CloudAPI().BService().Restore(ctx, restoreReq)
			if err != nil {
				return diag.FromErr(err)
			}

			hasChanged = true

			if d.Get("enable").(bool) {
				enableReq := bservice.EnableRequest{
					ServiceID: bs.ID,
				}
				_, err = c.CloudAPI().BService().Enable(ctx, enableReq)
				if err != nil {
					return diag.FromErr(err)
				}
			}

			if d.Get("start").(bool) {
				startReq := bservice.StartRequest{
					ServiceID: bs.ID,
				}
				_, err = c.CloudAPI().BService().Start(ctx, startReq)
				if err != nil {
					return diag.FromErr(err)
				}
			}
		}
	case status.Deleting:
	case status.Destroyed:
		d.SetId("")
		return diag.Errorf("The resource cannot be updated because it has been destroyed")
		// return resourceBasicServiceCreate(ctx, d, m)
	case status.Destroying:
		return diag.Errorf("The basic service is in progress with status: %s", bs.Status)
	case status.Restoring:
	case status.Reconfiguring:
	}

	if hasChanged {
		bs, err = utilityBasicServiceCheckPresence(ctx, d, m)
		if err != nil {
			d.SetId("")
			return diag.FromErr(err)
		}
	}

	if d.HasChange("enable") {
		if d.Get("enable").(bool) {
			req := bservice.EnableRequest{
				ServiceID: uint64(d.Get("service_id").(int)),
			}

			_, err := c.CloudAPI().BService().Enable(ctx, req)
			if err != nil {
				return diag.FromErr(err)
			}
		} else {
			req := bservice.DisableRequest{
				ServiceID: uint64(d.Get("service_id").(int)),
			}

			_, err := c.CloudAPI().BService().Disable(ctx, req)
			if err != nil {
				return diag.FromErr(err)
			}
		}
	}

	if d.HasChange("zone_id") {
		zoneID := uint64(d.Get("zone_id").(int))

		start := d.Get("start").(bool)
		if start {
			reqStop := bservice.StopRequest{
				ServiceID: uint64(d.Get("service_id").(int)),
			}

			_, err := c.CloudAPI().BService().Stop(ctx, reqStop)
			if err != nil {
				return diag.FromErr(err)
			}
		}

		req := bservice.MigrateToZoneRequest{
			ServiceID: uint64(d.Get("service_id").(int)),
			ZoneID:    zoneID,
		}

		_, err = c.CloudAPI().BService().MigrateToZone(ctx, req)
		if err != nil {
			return diag.FromErr(err)
		}

		if start {
			startReq := bservice.StartRequest{
				ServiceID: uint64(d.Get("service_id").(int)),
			}

			_, err = c.CloudAPI().BService().Start(ctx, startReq)
			if err != nil {
				return diag.FromErr(err)
			}
		}
	}

	if d.HasChange("start") {
		if d.Get("start").(bool) {
			req := bservice.StartRequest{
				ServiceID: uint64(d.Get("service_id").(int)),
			}

			_, err := c.CloudAPI().BService().Start(ctx, req)
			if err != nil {
				return diag.FromErr(err)
			}
		} else {
			req := bservice.StopRequest{
				ServiceID: uint64(d.Get("service_id").(int)),
			}

			_, err := c.CloudAPI().BService().Stop(ctx, req)
			if err != nil {
				return diag.FromErr(err)
			}
		}
	}

	if d.HasChange("snapshots") {
		deletedSnapshots := make([]interface{}, 0)
		addedSnapshots := make([]interface{}, 0)
		updatedSnapshots := make([]interface{}, 0)

		old, new := d.GetChange("snapshots")
		oldConv := old.([]interface{})
		newConv := new.([]interface{})
		for _, el := range oldConv {
			if !isContainsSnapshot(newConv, el) {
				deletedSnapshots = append(deletedSnapshots, el)
			}
		}
		for _, el := range newConv {
			if !isContainsSnapshot(oldConv, el) {
				addedSnapshots = append(addedSnapshots, el)
			} else {
				if isRollback(oldConv, el) {
					updatedSnapshots = append(updatedSnapshots, el)
				}
			}
		}

		if len(deletedSnapshots) > 0 {
			for _, snapshot := range deletedSnapshots {
				snapshotConv := snapshot.(map[string]interface{})
				req := bservice.SnapshotDeleteRequest{
					ServiceID: uint64(d.Get("service_id").(int)),
					Label:     snapshotConv["label"].(string),
				}

				_, err := c.CloudAPI().BService().SnapshotDelete(ctx, req)
				if err != nil {
					return diag.FromErr(err)
				}
			}
		}

		if len(addedSnapshots) > 0 {
			for _, snapshot := range addedSnapshots {
				snapshotConv := snapshot.(map[string]interface{})
				req := bservice.SnapshotCreateRequest{
					ServiceID: uint64(d.Get("service_id").(int)),
					Label:     snapshotConv["label"].(string),
				}

				_, err := c.CloudAPI().BService().SnapshotCreate(ctx, req)
				if err != nil {
					return diag.FromErr(err)
				}
			}
		}

		if len(updatedSnapshots) > 0 {
			for _, snapshot := range updatedSnapshots {
				snapshotConv := snapshot.(map[string]interface{})
				req := bservice.SnapshotRollbackRequest{
					ServiceID: uint64(d.Get("service_id").(int)),
					Label:     snapshotConv["label"].(string),
				}

				_, err := c.CloudAPI().BService().SnapshotRollback(ctx, req)
				if err != nil {
					return diag.FromErr(err)
				}
			}
		}
	}

	return resourceBasicServiceRead(ctx, d, m)
}

func isContainsSnapshot(els []interface{}, el interface{}) bool {
	for _, elOld := range els {
		elOldConv := elOld.(map[string]interface{})
		elConv := el.(map[string]interface{})
		if elOldConv["guid"].(string) == elConv["guid"].(string) {
			return true
		}
	}
	return false
}

func isRollback(els []interface{}, el interface{}) bool {
	for _, elOld := range els {
		elOldConv := elOld.(map[string]interface{})
		elConv := el.(map[string]interface{})
		if elOldConv["guid"].(string) == elConv["guid"].(string) &&
			elOldConv["rollback"].(bool) != elConv["rollback"].(bool) &&
			elConv["rollback"].(bool) {
			return true
		}
	}
	return false
}

func resourceBasicServiceSchemaMake() map[string]*schema.Schema {
	return map[string]*schema.Schema{
		"service_name": {
			Type:        schema.TypeString,
			Required:    true,
			Description: "Name of the service",
		},
		"rg_id": {
			Type:        schema.TypeInt,
			Required:    true,
			Description: "ID of the Resource Group where this service will be placed",
		},
		"zone_id": {
			Type:        schema.TypeInt,
			Optional:    true,
			Computed:    true,
			Description: "ID of the zone where this service will be placed",
		},
		"ssh_key": {
			Type:        schema.TypeString,
			Optional:    true,
			Computed:    true,
			Description: "SSH key to deploy for the specified user. Same key will be deployed to all computes of the service.",
		},
		"ssh_user": {
			Type:        schema.TypeString,
			Optional:    true,
			Computed:    true,
			Description: "name of the user to deploy SSH key for. Pass empty string if no SSH key deployment is required",
		},
		"permanently": {
			Type:        schema.TypeBool,
			Optional:    true,
			Default:     false,
			Description: "if set to False, Basic service will be deleted to recycle bin. Otherwise destroyed immediately",
		},
		"enable": {
			Type:        schema.TypeBool,
			Optional:    true,
			Default:     false,
			Description: "Enable service. Enabling a service technically means setting model status of all computes and service itself to ENABLED. It does not start computes.",
		},
		"restore": {
			Type:        schema.TypeBool,
			Optional:    true,
			Default:     false,
			Description: "Restores BasicService instance",
		},
		"start": {
			Type:        schema.TypeBool,
			Optional:    true,
			Default:     false,
			Description: "Start service. Starting a service technically means starting computes from all service groups according to group relations",
		},
		"service_id": {
			Type:     schema.TypeInt,
			Optional: true,
			Computed: true,
		},
		"account_id": {
			Type:     schema.TypeInt,
			Computed: true,
		},
		"account_name": {
			Type:     schema.TypeString,
			Computed: true,
		},
		"base_domain": {
			Type:     schema.TypeString,
			Computed: true,
		},
		"computes": {
			Type:     schema.TypeList,
			Computed: true,
			Elem: &schema.Resource{
				Schema: map[string]*schema.Schema{
					"account_id": {
						Type:     schema.TypeInt,
						Computed: true,
					},
					"architecture": {
						Type:     schema.TypeString,
						Computed: true,
					},
					"compgroup_id": {
						Type:     schema.TypeInt,
						Computed: true,
					},
					"compgroup_name": {
						Type:     schema.TypeString,
						Computed: true,
					},
					"compgroup_role": {
						Type:     schema.TypeString,
						Computed: true,
					},
					"id": {
						Type:     schema.TypeInt,
						Computed: true,
					},
					"rg_id": {
						Type:     schema.TypeInt,
						Computed: true,
					},
					"node_id": {
						Type:     schema.TypeInt,
						Computed: true,
					},
					"name": {
						Type:     schema.TypeString,
						Computed: true,
					},
					"status": {
						Type:     schema.TypeString,
						Computed: true,
					},
					"tech_status": {
						Type:     schema.TypeString,
						Computed: true,
					},
				},
			},
		},
		"cpu_total": {
			Type:     schema.TypeInt,
			Computed: true,
		},
		"created_by": {
			Type:     schema.TypeString,
			Computed: true,
		},
		"created_time": {
			Type:     schema.TypeInt,
			Computed: true,
		},
		"deleted_by": {
			Type:     schema.TypeString,
			Computed: true,
		},
		"deleted_time": {
			Type:     schema.TypeInt,
			Computed: true,
		},
		"disk_total": {
			Type:     schema.TypeInt,
			Computed: true,
		},
		"gid": {
			Type:     schema.TypeInt,
			Computed: true,
		},
		"groups": {
			Type:     schema.TypeList,
			Computed: true,
			Elem: &schema.Resource{
				Schema: map[string]*schema.Schema{
					"computes": {
						Type:     schema.TypeInt,
						Computed: true,
					},
					"consistency": {
						Type:     schema.TypeBool,
						Computed: true,
					},
					"id": {
						Type:     schema.TypeInt,
						Computed: true,
					},
					"name": {
						Type:     schema.TypeString,
						Computed: true,
					},
					"status": {
						Type:     schema.TypeString,
						Computed: true,
					},
					"tech_status": {
						Type:     schema.TypeString,
						Computed: true,
					},
				},
			},
		},
		"guid": {
			Type:     schema.TypeInt,
			Computed: true,
		},
		"milestones": {
			Type:     schema.TypeInt,
			Computed: true,
		},
		"parent_srv_id": {
			Type:     schema.TypeInt,
			Computed: true,
		},
		"parent_srv_type": {
			Type:     schema.TypeString,
			Computed: true,
		},
		"ram_total": {
			Type:     schema.TypeInt,
			Computed: true,
		},
		"rg_name": {
			Type:     schema.TypeString,
			Computed: true,
		},
		"snapshots": {
			Type:     schema.TypeList,
			Computed: true,
			Optional: true,
			Elem: &schema.Resource{
				Schema: map[string]*schema.Schema{
					"guid": {
						Type:     schema.TypeString,
						Computed: true,
					},
					"label": {
						Type:     schema.TypeString,
						Optional: true,
						Computed: true,
					},
					"rollback": {
						Type:     schema.TypeBool,
						Optional: true,
						Default:  false,
					},
					"timestamp": {
						Type:     schema.TypeInt,
						Computed: true,
					},
					"valid": {
						Type:     schema.TypeBool,
						Computed: true,
					},
				},
			},
		},
		"status": {
			Type:     schema.TypeString,
			Computed: true,
		},
		"tech_status": {
			Type:     schema.TypeString,
			Computed: true,
		},
		"updated_by": {
			Type:     schema.TypeString,
			Computed: true,
		},
		"updated_time": {
			Type:     schema.TypeInt,
			Computed: true,
		},
		"user_managed": {
			Type:     schema.TypeBool,
			Computed: true,
		},
	}
}

func ResourceBasicService() *schema.Resource {
	return &schema.Resource{
		SchemaVersion: 1,

		CreateContext: resourceBasicServiceCreate,
		ReadContext:   resourceBasicServiceRead,
		UpdateContext: resourceBasicServiceUpdate,
		DeleteContext: resourceBasicServiceDelete,

		Importer: &schema.ResourceImporter{
			StateContext: schema.ImportStatePassthroughContext,
		},

		Timeouts: &schema.ResourceTimeout{
			Create:  &constants.Timeout600s,
			Read:    &constants.Timeout300s,
			Update:  &constants.Timeout300s,
			Delete:  &constants.Timeout300s,
			Default: &constants.Timeout300s,
		},

		Schema: resourceBasicServiceSchemaMake(),
	}
}
