package oci

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

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

// BootVolume 是一个引导卷；引导卷由创建实例产生，本面板不单独创建。
// AttachedInstanceID / AttachedInstanceName 与 ImageName 在列表与详情查询时都会补全。
type BootVolume struct {
	ID                   string     `json:"id"`
	DisplayName          string     `json:"displayName"`
	LifecycleState       string     `json:"lifecycleState"`
	AvailabilityDomain   string     `json:"availabilityDomain"`
	CompartmentID        string     `json:"compartmentId"`
	SizeInGBs            int64      `json:"sizeInGBs"`
	VpusPerGB            int64      `json:"vpusPerGB"`
	ImageID              string     `json:"imageId"`
	ImageName            string     `json:"imageName,omitempty"`
	AttachedInstanceID   string     `json:"attachedInstanceId,omitempty"`
	AttachedInstanceName string     `json:"attachedInstanceName,omitempty"`
	TimeCreated          *time.Time `json:"timeCreated"`
}

// UpdateBootVolumeInput 是更新引导卷的输入；零值字段不更新。
// SizeInGBs 只能扩容，VpusPerGB 调整卷性能（10 均衡 / 20 高性能）。
type UpdateBootVolumeInput struct {
	Region      string
	DisplayName string
	SizeInGBs   int64
	VpusPerGB   int64
}

func (c *RealClient) blockstorageClient(cred Credentials, region string) (core.BlockstorageClient, error) {
	bc, err := core.NewBlockstorageClientWithConfigurationProvider(provider(cred))
	if err != nil {
		return bc, fmt.Errorf("new blockstorage client: %w", err)
	}
	applyProxy(&bc.BaseClient, cred)
	if region != "" {
		bc.SetRegion(normalizeRegion(region))
	}
	return bc, nil
}

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

// attachBootVolumeImages 批量补全来源镜像名：按 imageId 去重后逐个查询，
// 镜像已删除或查询失败时保持为空。
func (c *RealClient) attachBootVolumeImages(ctx context.Context, cred Credentials, region string, volumes []BootVolume) {
	names := map[string]string{}
	for _, v := range volumes {
		if v.ImageID != "" {
			names[v.ImageID] = ""
		}
	}
	for id := range names {
		img, err := c.GetImage(ctx, cred, region, id)
		if err != nil {
			continue
		}
		names[id] = img.DisplayName
	}
	for i := range volumes {
		volumes[i].ImageName = names[volumes[i].ImageID]
	}
}

// attachBootVolumeInstances 批量补全挂载实例：按 AD + compartment 分组各拉一次
// 全量挂载记录（免逐卷查询），构建 bootVolumeId → instanceId 映射后回填。
func (c *RealClient) attachBootVolumeInstances(ctx context.Context, cred Credentials, region string, volumes []BootVolume) {
	cc, err := c.computeClient(cred, region)
	if err != nil {
		return
	}
	type scope struct{ ad, comp string }
	attached := map[string]string{}
	seen := map[scope]bool{}
	for _, v := range volumes {
		s := scope{v.AvailabilityDomain, v.CompartmentID}
		if seen[s] {
			continue
		}
		seen[s] = true
		collectBootVolumeAttachments(ctx, cc, s.ad, hostCompartment(cred, s.comp), attached)
		collectVolumeAttachedBootVolumes(ctx, cc, s.ad, hostCompartment(cred, s.comp), attached)
	}
	names := instanceNames(ctx, cc, attached)
	for i := range volumes {
		id := attached[volumes[i].ID]
		volumes[i].AttachedInstanceID = id
		volumes[i].AttachedInstanceName = names[id]
	}
}

// instanceNames 按去重后的实例 ID 批量查询显示名；查询失败的留空。
func instanceNames(ctx context.Context, cc core.ComputeClient, attached map[string]string) map[string]string {
	names := map[string]string{}
	for _, instID := range attached {
		if instID == "" || names[instID] != "" {
			continue
		}
		resp, err := cc.GetInstance(ctx, core.GetInstanceRequest{InstanceId: &instID})
		if err != nil {
			continue
		}
		names[instID] = deref(resp.DisplayName)
	}
	return names
}

// collectVolumeAttachedBootVolumes 补齐「引导卷作数据卷附加」的挂载：这类挂载
// 记录在块卷挂载列表（volumeId 为引导卷 OCID），引导卷挂载列表查不到。
func collectVolumeAttachedBootVolumes(ctx context.Context, cc core.ComputeClient, ad string, compartmentID *string, out map[string]string) {
	var page *string
	for {
		resp, err := cc.ListVolumeAttachments(ctx, core.ListVolumeAttachmentsRequest{
			AvailabilityDomain: &ad,
			CompartmentId:      compartmentID,
			Page:               page,
		})
		if err != nil {
			return
		}
		for _, att := range resp.Items {
			id := deref(att.GetVolumeId())
			if att.GetLifecycleState() == core.VolumeAttachmentLifecycleStateAttached &&
				strings.Contains(id, ".bootvolume.") {
				out[id] = deref(att.GetInstanceId())
			}
		}
		if resp.OpcNextPage == nil {
			return
		}
		page = resp.OpcNextPage
	}
}

// collectBootVolumeAttachments 把一个 AD + compartment 下 ATTACHED 状态的挂载写入映射。
func collectBootVolumeAttachments(ctx context.Context, cc core.ComputeClient, ad string, compartmentID *string, out map[string]string) {
	var page *string
	for {
		resp, err := cc.ListBootVolumeAttachments(ctx, core.ListBootVolumeAttachmentsRequest{
			AvailabilityDomain: &ad,
			CompartmentId:      compartmentID,
			Page:               page,
		})
		if err != nil {
			return
		}
		for _, att := range resp.Items {
			if att.LifecycleState == core.BootVolumeAttachmentLifecycleStateAttached {
				out[deref(att.BootVolumeId)] = deref(att.InstanceId)
			}
		}
		if resp.OpcNextPage == nil {
			return
		}
		page = resp.OpcNextPage
	}
}

// GetBootVolume 实现 Client：查询引导卷并补全挂载的实例。
func (c *RealClient) GetBootVolume(ctx context.Context, cred Credentials, region, bootVolumeID string) (BootVolume, error) {
	bc, err := c.blockstorageClient(cred, region)
	if err != nil {
		return BootVolume{}, err
	}
	resp, err := bc.GetBootVolume(ctx, core.GetBootVolumeRequest{BootVolumeId: &bootVolumeID})
	if err != nil {
		return BootVolume{}, fmt.Errorf("get boot volume %s: %w", bootVolumeID, err)
	}
	volume := toBootVolume(resp.BootVolume)
	volume.AttachedInstanceID = c.bootVolumeAttachedInstance(ctx, cred, region, volume)
	if volume.ImageID != "" {
		if img, err := c.GetImage(ctx, cred, region, volume.ImageID); err == nil {
			volume.ImageName = img.DisplayName
		}
	}
	return volume, nil
}

// bootVolumeAttachedInstance 查询引导卷当前挂载的实例；查询失败视为未挂载。
func (c *RealClient) bootVolumeAttachedInstance(ctx context.Context, cred Credentials, region string, volume BootVolume) string {
	cc, err := c.computeClient(cred, region)
	if err != nil {
		return ""
	}
	// 挂载记录在引导卷所在 compartment，传租户根会漏掉子 compartment 的挂载
	resp, err := cc.ListBootVolumeAttachments(ctx, core.ListBootVolumeAttachmentsRequest{
		AvailabilityDomain: &volume.AvailabilityDomain,
		CompartmentId:      hostCompartment(cred, volume.CompartmentID),
		BootVolumeId:       &volume.ID,
	})
	if err != nil {
		return ""
	}
	for _, att := range resp.Items {
		if att.LifecycleState == core.BootVolumeAttachmentLifecycleStateAttached {
			return deref(att.InstanceId)
		}
	}
	return ""
}

// UpdateBootVolume 实现 Client。
func (c *RealClient) UpdateBootVolume(ctx context.Context, cred Credentials, bootVolumeID string, in UpdateBootVolumeInput) (BootVolume, error) {
	bc, err := c.blockstorageClient(cred, in.Region)
	if err != nil {
		return BootVolume{}, err
	}
	details := core.UpdateBootVolumeDetails{}
	if in.DisplayName != "" {
		details.DisplayName = &in.DisplayName
	}
	if in.SizeInGBs > 0 {
		details.SizeInGBs = &in.SizeInGBs
	}
	if in.VpusPerGB > 0 {
		details.VpusPerGB = &in.VpusPerGB
	}
	resp, err := bc.UpdateBootVolume(ctx, core.UpdateBootVolumeRequest{
		BootVolumeId:            &bootVolumeID,
		UpdateBootVolumeDetails: details,
	})
	if err != nil {
		return BootVolume{}, fmt.Errorf("update boot volume %s: %w", bootVolumeID, err)
	}
	return toBootVolume(resp.BootVolume), nil
}

// DeleteBootVolume 实现 Client；仅未挂载（detached）的引导卷可删除。
func (c *RealClient) DeleteBootVolume(ctx context.Context, cred Credentials, region, bootVolumeID string) error {
	bc, err := c.blockstorageClient(cred, region)
	if err != nil {
		return err
	}
	if _, err := bc.DeleteBootVolume(ctx, core.DeleteBootVolumeRequest{BootVolumeId: &bootVolumeID}); err != nil {
		return fmt.Errorf("delete boot volume %s: %w", bootVolumeID, err)
	}
	return nil
}

func toBootVolume(v core.BootVolume) BootVolume {
	out := BootVolume{
		ID:                 deref(v.Id),
		DisplayName:        deref(v.DisplayName),
		LifecycleState:     string(v.LifecycleState),
		AvailabilityDomain: deref(v.AvailabilityDomain),
		CompartmentID:      deref(v.CompartmentId),
		ImageID:            deref(v.ImageId),
		TimeCreated:        sdkTime(v.TimeCreated),
	}
	if v.SizeInGBs != nil {
		out.SizeInGBs = *v.SizeInGBs
	}
	if v.VpusPerGB != nil {
		out.VpusPerGB = *v.VpusPerGB
	}
	return out
}
