package oci

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/oracle/oci-go-sdk/v65/common"
	"github.com/oracle/oci-go-sdk/v65/core"
)

// BootVolumeAttachment 是引导卷与实例的挂载关系。
type BootVolumeAttachment struct {
	ID                 string `json:"id"`
	InstanceID         string `json:"instanceId"`
	BootVolumeID       string `json:"bootVolumeId"`
	LifecycleState     string `json:"lifecycleState"`
	DisplayName        string `json:"displayName"`
	AvailabilityDomain string `json:"availabilityDomain"`
}

// VolumeAttachment 是块存储卷与实例的挂载关系。
type VolumeAttachment struct {
	ID                 string `json:"id"`
	InstanceID         string `json:"instanceId"`
	VolumeID           string `json:"volumeId"`
	VolumeName         string `json:"volumeName,omitempty"` // 卷本身的名称（attachment 名是自动生成的）
	LifecycleState     string `json:"lifecycleState"`
	DisplayName        string `json:"displayName"`
	Device             string `json:"device"`
	IsReadOnly         bool   `json:"isReadOnly"`
	AvailabilityDomain string `json:"availabilityDomain"`
}

// BlockVolume 是一个块存储卷。
type BlockVolume struct {
	ID                 string     `json:"id"`
	DisplayName        string     `json:"displayName"`
	LifecycleState     string     `json:"lifecycleState"`
	AvailabilityDomain string     `json:"availabilityDomain"`
	SizeInGBs          int64      `json:"sizeInGBs"`
	VpusPerGB          int64      `json:"vpusPerGB"`
	TimeCreated        *time.Time `json:"timeCreated"`
}

// ---- 引导卷挂载 ----

// ListBootVolumeAttachments 实现 Client：列出实例的引导卷挂载。
func (c *RealClient) ListBootVolumeAttachments(ctx context.Context, cred Credentials, region, instanceID string) ([]BootVolumeAttachment, error) {
	cc, err := c.computeClient(cred, region)
	if err != nil {
		return nil, err
	}
	instResp, err := cc.GetInstance(ctx, core.GetInstanceRequest{InstanceId: &instanceID})
	if err != nil {
		return nil, fmt.Errorf("get instance %s: %w", instanceID, err)
	}
	// 挂载记录在实例所在 compartment，复用上面查到的实例定位，传租户根会漏查
	resp, err := cc.ListBootVolumeAttachments(ctx, core.ListBootVolumeAttachmentsRequest{
		AvailabilityDomain: instResp.AvailabilityDomain,
		CompartmentId:      hostCompartment(cred, deref(instResp.CompartmentId)),
		InstanceId:         &instanceID,
	})
	if err != nil {
		return nil, fmt.Errorf("list boot volume attachments: %w", err)
	}
	atts := make([]BootVolumeAttachment, 0, len(resp.Items))
	for _, item := range resp.Items {
		atts = append(atts, toBootVolumeAttachment(item))
	}
	return atts, nil
}

// AttachBootVolume 实现 Client；实例需处于 STOPPED，引导卷与实例同可用域。
func (c *RealClient) AttachBootVolume(ctx context.Context, cred Credentials, region, instanceID, bootVolumeID string) (BootVolumeAttachment, error) {
	cc, err := c.computeClient(cred, region)
	if err != nil {
		return BootVolumeAttachment{}, err
	}
	resp, err := cc.AttachBootVolume(ctx, core.AttachBootVolumeRequest{
		AttachBootVolumeDetails: core.AttachBootVolumeDetails{
			InstanceId:   &instanceID,
			BootVolumeId: &bootVolumeID,
		},
	})
	if err != nil {
		return BootVolumeAttachment{}, fmt.Errorf("attach boot volume: %w", err)
	}
	return toBootVolumeAttachment(resp.BootVolumeAttachment), nil
}

// DetachBootVolume 实现 Client；实例需处于 STOPPED。
func (c *RealClient) DetachBootVolume(ctx context.Context, cred Credentials, region, attachmentID string) error {
	cc, err := c.computeClient(cred, region)
	if err != nil {
		return err
	}
	if _, err := cc.DetachBootVolume(ctx, core.DetachBootVolumeRequest{BootVolumeAttachmentId: &attachmentID}); err != nil {
		return fmt.Errorf("detach boot volume %s: %w", attachmentID, err)
	}
	return nil
}

// ReplaceBootVolume 实现 Client：分离实例当前引导卷，挂载新引导卷。
// 实例必须处于 STOPPED，全程等待分离完成后再挂载。
func (c *RealClient) ReplaceBootVolume(ctx context.Context, cred Credentials, region, instanceID, newBootVolumeID string) (BootVolumeAttachment, error) {
	cc, err := c.computeClient(cred, region)
	if err != nil {
		return BootVolumeAttachment{}, err
	}
	instResp, err := cc.GetInstance(ctx, core.GetInstanceRequest{InstanceId: &instanceID})
	if err != nil {
		return BootVolumeAttachment{}, fmt.Errorf("get instance %s: %w", instanceID, err)
	}
	if instResp.LifecycleState != core.InstanceLifecycleStateStopped {
		return BootVolumeAttachment{}, fmt.Errorf("replace boot volume: instance must be STOPPED, current %s", instResp.LifecycleState)
	}
	atts, err := c.ListBootVolumeAttachments(ctx, cred, region, instanceID)
	if err != nil {
		return BootVolumeAttachment{}, err
	}
	for _, att := range atts {
		if att.LifecycleState != string(core.BootVolumeAttachmentLifecycleStateDetached) {
			if err := c.DetachBootVolume(ctx, cred, region, att.ID); err != nil {
				return BootVolumeAttachment{}, err
			}
			if err := waitBootVolumeDetached(ctx, cc, att.ID); err != nil {
				return BootVolumeAttachment{}, err
			}
		}
	}
	return c.AttachBootVolume(ctx, cred, region, instanceID, newBootVolumeID)
}

// waitBootVolumeDetached 轮询等待引导卷挂载进入 DETACHED；
// OCI 可能在分离完成后回收挂载记录，404 同样视为已分离。
func waitBootVolumeDetached(ctx context.Context, cc core.ComputeClient, attachmentID string) error {
	for i := 0; i < 60; i++ {
		resp, err := cc.GetBootVolumeAttachment(ctx, core.GetBootVolumeAttachmentRequest{BootVolumeAttachmentId: &attachmentID})
		if err != nil {
			var svcErr common.ServiceError
			if errors.As(err, &svcErr) && svcErr.GetHTTPStatusCode() == 404 {
				return nil
			}
			return fmt.Errorf("poll boot volume attachment: %w", err)
		}
		if resp.LifecycleState == core.BootVolumeAttachmentLifecycleStateDetached {
			return nil
		}
		select {
		case <-ctx.Done():
			return fmt.Errorf("poll boot volume detached: %w", ctx.Err())
		case <-time.After(3 * time.Second):
		}
	}
	return fmt.Errorf("poll boot volume detached: timed out")
}

// ---- 块卷挂载 ----

// ListVolumeAttachments 实现 Client：列出实例的块卷挂载。
func (c *RealClient) ListVolumeAttachments(ctx context.Context, cred Credentials, region, instanceID string) ([]VolumeAttachment, error) {
	cc, err := c.computeClient(cred, region)
	if err != nil {
		return nil, err
	}
	req := core.ListVolumeAttachmentsRequest{CompartmentId: cred.EffectiveCompartment()}
	if instanceID != "" {
		// 挂载记录在实例所在 compartment，先取实例定位，传租户根会漏查
		instResp, err := cc.GetInstance(ctx, core.GetInstanceRequest{InstanceId: &instanceID})
		if err != nil {
			return nil, fmt.Errorf("get instance %s: %w", instanceID, err)
		}
		req.CompartmentId = hostCompartment(cred, deref(instResp.CompartmentId))
		req.InstanceId = &instanceID
	}
	resp, err := cc.ListVolumeAttachments(ctx, req)
	if err != nil {
		return nil, fmt.Errorf("list volume attachments: %w", err)
	}
	atts := make([]VolumeAttachment, 0, len(resp.Items))
	for _, item := range resp.Items {
		atts = append(atts, toVolumeAttachment(item))
	}
	c.attachVolumeNames(ctx, cred, region, atts)
	return atts, nil
}

// attachVolumeNames 补全每条挂载的卷名（attachment 名是自动生成的无意义串）；
// 引导卷可作数据卷附加，按 OCID 类型分别查询，查询失败留空不影响列表。
func (c *RealClient) attachVolumeNames(ctx context.Context, cred Credentials, region string, atts []VolumeAttachment) {
	bc, err := c.blockstorageClient(cred, region)
	if err != nil {
		return
	}
	names := map[string]string{}
	for i := range atts {
		id := atts[i].VolumeID
		if id == "" {
			continue
		}
		name, ok := names[id]
		if !ok {
			name = lookupVolumeName(ctx, bc, id)
			names[id] = name
		}
		atts[i].VolumeName = name
	}
}

func lookupVolumeName(ctx context.Context, bc core.BlockstorageClient, id string) string {
	if strings.Contains(id, ".bootvolume.") {
		resp, err := bc.GetBootVolume(ctx, core.GetBootVolumeRequest{BootVolumeId: &id})
		if err != nil {
			return ""
		}
		return deref(resp.DisplayName)
	}
	resp, err := bc.GetVolume(ctx, core.GetVolumeRequest{VolumeId: &id})
	if err != nil {
		return ""
	}
	return deref(resp.DisplayName)
}

// AttachVolume 实现 Client：以半虚拟化方式挂载块卷（无需 iSCSI 配置）。
func (c *RealClient) AttachVolume(ctx context.Context, cred Credentials, region, instanceID, volumeID string, readOnly bool) (VolumeAttachment, error) {
	cc, err := c.computeClient(cred, region)
	if err != nil {
		return VolumeAttachment{}, err
	}
	resp, err := cc.AttachVolume(ctx, core.AttachVolumeRequest{
		AttachVolumeDetails: core.AttachParavirtualizedVolumeDetails{
			InstanceId: &instanceID,
			VolumeId:   &volumeID,
			IsReadOnly: &readOnly,
		},
	})
	if err != nil {
		return VolumeAttachment{}, fmt.Errorf("attach volume: %w", err)
	}
	return toVolumeAttachment(resp.VolumeAttachment), nil
}

// DetachVolume 实现 Client。
func (c *RealClient) DetachVolume(ctx context.Context, cred Credentials, region, attachmentID string) error {
	cc, err := c.computeClient(cred, region)
	if err != nil {
		return err
	}
	if _, err := cc.DetachVolume(ctx, core.DetachVolumeRequest{VolumeAttachmentId: &attachmentID}); err != nil {
		return fmt.Errorf("detach volume %s: %w", attachmentID, err)
	}
	return nil
}

// ListBlockVolumes 实现 Client；availabilityDomain 为空时列出全区域块卷。
func (c *RealClient) ListBlockVolumes(ctx context.Context, cred Credentials, region, availabilityDomain string) ([]BlockVolume, error) {
	bc, err := c.blockstorageClient(cred, region)
	if err != nil {
		return nil, err
	}
	var volumes []BlockVolume
	var page *string
	for {
		req := core.ListVolumesRequest{CompartmentId: cred.EffectiveCompartment(), Page: page}
		if availabilityDomain != "" {
			req.AvailabilityDomain = &availabilityDomain
		}
		resp, err := bc.ListVolumes(ctx, req)
		if err != nil {
			return nil, fmt.Errorf("list volumes: %w", err)
		}
		for _, item := range resp.Items {
			volumes = append(volumes, toBlockVolume(item))
		}
		if resp.OpcNextPage == nil {
			return orEmpty(volumes), nil
		}
		page = resp.OpcNextPage
	}
}

func toBootVolumeAttachment(a core.BootVolumeAttachment) BootVolumeAttachment {
	return BootVolumeAttachment{
		ID:                 deref(a.Id),
		InstanceID:         deref(a.InstanceId),
		BootVolumeID:       deref(a.BootVolumeId),
		LifecycleState:     string(a.LifecycleState),
		DisplayName:        deref(a.DisplayName),
		AvailabilityDomain: deref(a.AvailabilityDomain),
	}
}

func toVolumeAttachment(a core.VolumeAttachment) VolumeAttachment {
	out := VolumeAttachment{
		ID:                 deref(a.GetId()),
		InstanceID:         deref(a.GetInstanceId()),
		VolumeID:           deref(a.GetVolumeId()),
		LifecycleState:     string(a.GetLifecycleState()),
		DisplayName:        deref(a.GetDisplayName()),
		Device:             deref(a.GetDevice()),
		AvailabilityDomain: deref(a.GetAvailabilityDomain()),
	}
	if ro := a.GetIsReadOnly(); ro != nil {
		out.IsReadOnly = *ro
	}
	return out
}

func toBlockVolume(v core.Volume) BlockVolume {
	out := BlockVolume{
		ID:                 deref(v.Id),
		DisplayName:        deref(v.DisplayName),
		LifecycleState:     string(v.LifecycleState),
		AvailabilityDomain: deref(v.AvailabilityDomain),
		TimeCreated:        sdkTime(v.TimeCreated),
	}
	if v.SizeInGBs != nil {
		out.SizeInGBs = *v.SizeInGBs
	}
	if v.VpusPerGB != nil {
		out.VpusPerGB = *v.VpusPerGB
	}
	return out
}
