// Copyright 2022-2025 The sacloud/iaas-api-go Authors
//
// 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.

package fake

import (
	"context"
	"fmt"
	"time"

	"github.com/sacloud/iaas-api-go"
	"github.com/sacloud/iaas-api-go/types"
)

// Find is fake implementation
func (o *DiskOp) Find(ctx context.Context, zone string, conditions *iaas.FindCondition) (*iaas.DiskFindResult, error) {
	results, _ := find(o.key, zone, conditions)
	var values []*iaas.Disk
	for _, res := range results {
		dest := &iaas.Disk{}
		copySameNameField(res, dest)
		values = append(values, dest)
	}
	return &iaas.DiskFindResult{
		Total: len(results),
		Count: len(results),
		From:  0,
		Disks: values,
	}, nil
}

// Create is fake implementation
func (o *DiskOp) Create(ctx context.Context, zone string, param *iaas.DiskCreateRequest, distantFrom []types.ID, kmsKeyID types.ID) (*iaas.Disk, error) {
	result := &iaas.Disk{}
	copySameNameField(param, result)
	fill(result, fillID, fillCreatedAt, fillDiskPlan)
	result.Availability = types.Availabilities.Migrating
	result.KMSKeyID = kmsKeyID

	result.Storage = &iaas.Storage{
		ID:   types.ID(123456789012),
		Name: "dummy",
	}

	if result.Connection == types.EDiskConnection("") {
		result.Connection = types.DiskConnections.VirtIO
	}
	if !param.SourceArchiveID.IsEmpty() {
		archiveOp := NewArchiveOp()
		source, err := archiveOp.Read(ctx, zone, param.SourceArchiveID)
		if err != nil {
			return nil, newErrorBadRequest(o.key, types.ID(0), "SourceArchive is not found")
		}
		result.SourceArchiveAvailability = source.Availability
	}
	if !param.SourceDiskID.IsEmpty() {
		source, err := o.Read(ctx, zone, param.SourceDiskID)
		if err != nil {
			return nil, newErrorBadRequest(o.key, types.ID(0), "SourceDisk is not found")
		}
		result.SourceDiskAvailability = source.Availability
	}
	if !param.ServerID.IsEmpty() {
		server, err := NewServerOp().Read(ctx, zone, param.ServerID)
		if err != nil {
			return nil, newErrorConflict(o.key, types.ID(0), "Server is not found")
		}
		server.Disks = append(server.Disks, &iaas.ServerConnectedDisk{
			ID:              result.ID,
			Name:            result.Name,
			Availability:    result.Availability,
			Connection:      result.Connection,
			ConnectionOrder: result.ConnectionOrder,
			ReinstallCount:  result.ReinstallCount,
			SizeMB:          result.SizeMB,
			DiskPlanID:      result.DiskPlanID,
			Storage:         result.Storage,
		})
		putServer(zone, server)
	}

	putDisk(zone, result)

	id := result.ID
	startDiskCopy(o.key, zone, func() (interface{}, error) {
		disk, err := o.Read(context.Background(), zone, id)
		if err != nil {
			return nil, err
		}
		return disk, nil
	})

	return result, nil
}

// Config is fake implementation
func (o *DiskOp) Config(ctx context.Context, zone string, id types.ID, edit *iaas.DiskEditRequest) error {
	disk, err := o.Read(ctx, zone, id)
	if err != nil {
		return err
	}
	if disk.ServerID.IsEmpty() {
		return nil
	}

	serverOp := NewServerOp()
	server, err := serverOp.Read(ctx, zone, disk.ServerID)
	if err != nil {
		return err
	}

	if edit.HostName != "" {
		server.HostName = edit.HostName
		putServer(zone, server)
	}

	if len(server.Interfaces) > 0 {
		nic := server.Interfaces[0]
		if nic.SwitchScope == types.Scopes.Shared {
			nic.IPAddress = pool().nextSharedIP().String()
		} else {
			nic.UserIPAddress = edit.UserIPAddress
		}

		swOp := NewSwitchOp()
		sw, err := swOp.Read(ctx, zone, nic.SwitchID)
		if err != nil {
			return err
		}

		if len(sw.Subnets) == 0 {
			nic.UserSubnetDefaultRoute = edit.UserSubnet.DefaultRoute
			nic.UserSubnetNetworkMaskLen = edit.UserSubnet.NetworkMaskLen
		} else {
			nic.UserSubnetDefaultRoute = sw.Subnets[0].DefaultRoute
			nic.UserSubnetNetworkMaskLen = sw.Subnets[0].NetworkMaskLen
			nic.SubnetDefaultRoute = sw.Subnets[0].DefaultRoute
			nic.SubnetNetworkAddress = sw.Subnets[0].NetworkAddress
		}

		putServer(zone, server)
	}

	return nil
}

// CreateWithConfig is fake implementation
func (o *DiskOp) CreateWithConfig(ctx context.Context, zone string, createParam *iaas.DiskCreateRequest, editParam *iaas.DiskEditRequest, bootAtAvailable bool, distantFrom []types.ID, kmsKeyID types.ID) (*iaas.Disk, error) {
	// check
	if !createParam.ServerID.IsEmpty() {
		serverOp := NewServerOp()
		_, err := serverOp.Read(ctx, zone, createParam.ServerID)
		if err != nil {
			return nil, newErrorBadRequest(o.key, types.ID(0), fmt.Sprintf("Server %s is not found", createParam.ServerID))
		}
	}

	result, err := o.Create(ctx, zone, createParam, distantFrom, kmsKeyID)
	if err != nil {
		return nil, err
	}

	if err := o.Config(ctx, zone, result.ID, editParam); err != nil {
		return nil, err
	}

	if !createParam.ServerID.IsEmpty() && bootAtAvailable {
		waiter := iaas.WaiterForReady(func() (interface{}, error) {
			disk, err := o.Read(ctx, zone, result.ID)
			if err != nil {
				return nil, err
			}
			return disk, nil
		})
		res, err := waiter.WaitForState(ctx)
		if err != nil {
			return nil, err
		}
		result = res.(*iaas.Disk)

		// boot server
		serverOp := NewServerOp()
		if err := serverOp.Boot(ctx, zone, createParam.ServerID); err != nil {
			return nil, err
		}
	}
	return result, nil
}

// ResizePartition is fake implementation
func (o *DiskOp) ResizePartition(ctx context.Context, zone string, id types.ID, param *iaas.DiskResizePartitionRequest) error {
	_, err := o.Read(ctx, zone, id)
	if err != nil {
		return err
	}
	return nil
}

// ConnectToServer is fake implementation
func (o *DiskOp) ConnectToServer(ctx context.Context, zone string, id types.ID, serverID types.ID) error {
	value, err := o.Read(ctx, zone, id)
	if err != nil {
		return err
	}

	serverOp := NewServerOp()
	server, err := serverOp.Read(ctx, zone, serverID)
	if err != nil {
		return newErrorBadRequest(o.key, id, fmt.Sprintf("Server[%d] is not exists", serverID))
	}

	for _, connected := range server.Disks {
		if connected.ID == value.ID {
			return newErrorBadRequest(o.key, id, fmt.Sprintf("Disk[%d] is already connected to Server[%d]", id, serverID))
		}
	}

	// TODO とりあえず同時実行制御は考慮しない。更新対象リソースが増えるようであれば実装方法を考える

	connectedDisk := &iaas.ServerConnectedDisk{}
	copySameNameField(value, connectedDisk)
	server.Disks = append(server.Disks, connectedDisk)
	putServer(zone, server)
	value.ServerID = serverID
	value.ServerName = server.Name
	putDisk(zone, value)

	return nil
}

// DisconnectFromServer is fake implementation
func (o *DiskOp) DisconnectFromServer(ctx context.Context, zone string, id types.ID) error {
	value, err := o.Read(ctx, zone, id)
	if err != nil {
		return err
	}

	if value.ServerID.IsEmpty() {
		return newErrorBadRequest(o.key, id, fmt.Sprintf("Disk[%d] is not connected to Server", id))
	}

	serverOp := NewServerOp()
	server, err := serverOp.Read(ctx, zone, value.ServerID)
	if err != nil {
		return newErrorBadRequest(o.key, id, fmt.Sprintf("Server[%d] is not exists", value.ServerID))
	}

	var disks []*iaas.ServerConnectedDisk
	for _, connected := range server.Disks {
		if connected.ID != value.ID {
			connectedDisk := &iaas.ServerConnectedDisk{}
			copySameNameField(value, connectedDisk)
			server.Disks = append(server.Disks, connectedDisk)
			disks = append(disks, connected)
		}
	}
	if len(disks) == len(server.Disks) {
		return newInternalServerError(o.key, id, fmt.Sprintf("Disk[%d] is not found on server's connected disks", id))
	}

	server.Disks = disks
	putServer(zone, server)
	value.ServerID = types.ID(0)
	value.ServerName = ""
	putDisk(zone, value)

	return nil
}

// Read is fake implementation
func (o *DiskOp) Read(ctx context.Context, zone string, id types.ID) (*iaas.Disk, error) {
	value := getDiskByID(zone, id)
	if value == nil {
		return nil, newErrorNotFound(o.key, id)
	}

	dest := &iaas.Disk{}
	copySameNameField(value, dest)
	return dest, nil
}

// Update is fake implementation
func (o *DiskOp) Update(ctx context.Context, zone string, id types.ID, param *iaas.DiskUpdateRequest) (*iaas.Disk, error) {
	value, err := o.Read(ctx, zone, id)
	if err != nil {
		return nil, err
	}
	copySameNameField(param, value)
	fill(value, fillModifiedAt)

	putDisk(zone, value)
	return value, nil
}

// Delete is fake implementation
func (o *DiskOp) Delete(ctx context.Context, zone string, id types.ID) error {
	_, err := o.Read(ctx, zone, id)
	if err != nil {
		return err
	}
	ds().Delete(o.key, zone, id)
	return nil
}

// Monitor is fake implementation
func (o *DiskOp) Monitor(ctx context.Context, zone string, id types.ID, condition *iaas.MonitorCondition) (*iaas.DiskActivity, error) {
	_, err := o.Read(ctx, zone, id)
	if err != nil {
		return nil, err
	}
	now := time.Now().Truncate(time.Second)
	m := now.Minute() % 5
	if m != 0 {
		now.Add(time.Duration(m) * time.Minute)
	}

	res := &iaas.DiskActivity{}
	for i := 0; i < 5; i++ {
		res.Values = append(res.Values, &iaas.MonitorDiskValue{
			Time:  now.Add(time.Duration(i*-5) * time.Minute),
			Read:  float64(random(1000)),
			Write: float64(random(1000)),
		})
	}

	return res, nil
}

// MonitorDisk is fake implementation
func (o *DiskOp) MonitorDisk(ctx context.Context, zone string, id types.ID, condition *iaas.MonitorCondition) (*iaas.DiskActivity, error) {
	return o.Monitor(ctx, zone, id, condition)
}
