// Package oci 封装 Oracle Cloud Infrastructure 官方 Go SDK，
// 对上层提供统一的客户端抽象；真实云请求只允许出现在本包。
package oci

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

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

	"oci-portal/internal/aiwire"
)

// cloudcmServiceName 是 OCI 云消费（Cloud Consumption Model）订阅的服务名，
// 账户类别信息挂在该订阅之下。
const cloudcmServiceName = "CLOUDCM"

// ErrNoCloudcmSubscription 表示租户下没有 serviceName=CLOUDCM 的订阅。
var ErrNoCloudcmSubscription = errors.New("no CLOUDCM subscription found")

// TenancyInfo 是测活成功后取得的租户基本信息。
type TenancyInfo struct {
	Name          string
	HomeRegionKey string
}

// Client 是真实 OCI 调用的统一入口，service 层只依赖此接口。
type Client interface {
	// ValidateKey 用 Identity GetTenancy 验证凭据有效性（测活）。
	ValidateKey(ctx context.Context, cred Credentials) (TenancyInfo, error)
	// FetchAccountProfile 查询 CLOUDCM 订阅并判定账户类别。
	FetchAccountProfile(ctx context.Context, cred Credentials) (AccountProfile, error)
	// ListCompartments 递归列出租户下全部 ACTIVE compartment（不含租户根）。
	ListCompartments(ctx context.Context, cred Credentials) ([]Compartment, error)
	// ListRegionSubscriptions 列出租户已订阅的区域。
	ListRegionSubscriptions(ctx context.Context, cred Credentials) ([]RegionSubscription, error)
	// SubscribeRegion 为租户订阅新区域；请求发往 homeRegion，操作不可撤销。
	SubscribeRegion(ctx context.Context, cred Credentials, homeRegion, regionKey string) error
	// ListLimits 查询服务配额，name 为大小写不敏感的模糊过滤，按需附带用量。
	ListLimits(ctx context.Context, cred Credentials, q LimitsQuery) ([]LimitValue, error)
	// ListLimitServices 列出配额体系支持的全部服务。
	ListLimitServices(ctx context.Context, cred Credentials, region string) ([]LimitService, error)
	// ListSubscriptions 列出租户全部订阅摘要。
	ListSubscriptions(ctx context.Context, cred Credentials) ([]SubscriptionInfo, error)
	// GetSubscription 查询单个订阅的完整信息。
	GetSubscription(ctx context.Context, cred Credentials, subscriptionID string) (SubscriptionDetail, error)
	// ListShapes 列出租户在目标区域可用的实例规格（带缓存）。
	ListShapes(ctx context.Context, cred Credentials, region, availabilityDomain string) ([]ComputeShape, error)
	// ListImages 列出可用的实例镜像。
	ListImages(ctx context.Context, cred Credentials, q ImagesQuery) ([]Image, error)
	// GetImage 查询单个镜像。
	GetImage(ctx context.Context, cred Credentials, region, imageID string) (Image, error)
	// VCN 增删改查与一键 IPv6。
	ListVCNs(ctx context.Context, cred Credentials, region string) ([]VCN, error)
	CreateVCN(ctx context.Context, cred Credentials, in CreateVCNInput) (VCN, error)
	GetVCN(ctx context.Context, cred Credentials, region, vcnID string) (VCN, error)
	UpdateVCN(ctx context.Context, cred Credentials, region, vcnID, displayName string) (VCN, error)
	DeleteVCN(ctx context.Context, cred Credentials, region, vcnID string) error
	EnableVCNIPv6(ctx context.Context, cred Credentials, region, vcnID string) ([]IPv6Step, error)
	// 子网增删改查。
	ListSubnets(ctx context.Context, cred Credentials, region, vcnID string) ([]Subnet, error)
	CreateSubnet(ctx context.Context, cred Credentials, in CreateSubnetInput) (Subnet, error)
	GetSubnet(ctx context.Context, cred Credentials, region, subnetID string) (Subnet, error)
	UpdateSubnet(ctx context.Context, cred Credentials, region, subnetID, displayName string) (Subnet, error)
	DeleteSubnet(ctx context.Context, cred Credentials, region, subnetID string) error
	// 安全列表增删改查。
	ListSecurityLists(ctx context.Context, cred Credentials, region, vcnID string) ([]SecurityList, error)
	CreateSecurityList(ctx context.Context, cred Credentials, in CreateSecurityListInput) (SecurityList, error)
	GetSecurityList(ctx context.Context, cred Credentials, region, securityListID string) (SecurityList, error)
	UpdateSecurityList(ctx context.Context, cred Credentials, securityListID string, in UpdateSecurityListInput) (SecurityList, error)
	DeleteSecurityList(ctx context.Context, cred Credentials, region, securityListID string) error
	// ListAvailabilityDomains 列出区域内可用域，创建实例的前置查询。
	ListAvailabilityDomains(ctx context.Context, cred Credentials, region string) ([]string, error)
	// 实例增删改查与电源操作。
	ListInstances(ctx context.Context, cred Credentials, region string) ([]Instance, error)
	LaunchInstance(ctx context.Context, cred Credentials, in CreateInstanceInput) (Instance, error)
	GetInstance(ctx context.Context, cred Credentials, region, instanceID string) (Instance, error)
	UpdateInstance(ctx context.Context, cred Credentials, instanceID string, in UpdateInstanceInput) (Instance, error)
	TerminateInstance(ctx context.Context, cred Credentials, region, instanceID string, preserveBootVolume bool) error
	InstanceAction(ctx context.Context, cred Credentials, region, instanceID, action string) (Instance, error)
	// 引导卷查删改；引导卷由创建实例产生，不单独创建。
	ListBootVolumes(ctx context.Context, cred Credentials, region, availabilityDomain string) ([]BootVolume, error)
	GetBootVolume(ctx context.Context, cred Credentials, region, bootVolumeID string) (BootVolume, error)
	UpdateBootVolume(ctx context.Context, cred Credentials, bootVolumeID string, in UpdateBootVolumeInput) (BootVolume, error)
	DeleteBootVolume(ctx context.Context, cred Credentials, region, bootVolumeID string) error
	// 实例 IP：更换主 VNIC 临时公网 IP、添加与取消分配 IPv6 地址。
	ChangeInstancePublicIP(ctx context.Context, cred Credentials, region, instanceID string) (string, error)
	AddInstanceIpv6(ctx context.Context, cred Credentials, region, instanceID, address string) (string, error)
	DeleteInstanceIpv6(ctx context.Context, cred Credentials, region, instanceID, address string) error
	// VNIC 管理:列出实例网卡、附加次要网卡、分离(主网卡由 OCI 拒绝)、按网卡加 IPv6。
	ListInstanceVnics(ctx context.Context, cred Credentials, region, instanceID string) ([]Vnic, error)
	AttachVnic(ctx context.Context, cred Credentials, region, instanceID string, in AttachVnicInput) (Vnic, error)
	DetachVnic(ctx context.Context, cred Credentials, region, attachmentID string) error
	AddVnicIpv6(ctx context.Context, cred Credentials, region, vnicID, address string) (string, error)
	// GenAI 网关:区域模型列表、聊天(非流式/流式)、配额探测、文本向量化。
	ListGenAiModels(ctx context.Context, cred Credentials, region string) ([]GenAiModel, error)
	GenAiProbeChat(ctx context.Context, cred Credentials, region, modelOcid, modelName string) (int, error)
	GenAiEmbed(ctx context.Context, cred Credentials, region, modelOcid string, inputs []string, dimensions *int) ([][]float32, *aiwire.Usage, error)
	// GenAiCompatResponses 直通 OpenAI Responses 请求体到 /actions/v1/responses(xAI 服务端工具通路);
	// wait 是上游无响应预算(整请求总超时),multi-agent/搜索类模型需远超 SDK 默认 60s。
	GenAiCompatResponses(ctx context.Context, cred Credentials, region string, body []byte, wait time.Duration) ([]byte, error)
	// GenAiCompatResponsesStream 流式直通 /actions/v1/responses,建立成功返回 SSE body;
	// wait 仅约束等待响应头阶段,建立后的流生命周期由 ctx 决定。
	GenAiCompatResponsesStream(ctx context.Context, cred Credentials, region string, body []byte, wait time.Duration) (io.ReadCloser, error)
	// GenAiCompatSpeech 直通 OpenAI Audio Speech 请求体到 /openai/v1/audio/speech,返回音频与 Content-Type。
	GenAiCompatSpeech(ctx context.Context, cred Credentials, region string, body []byte) ([]byte, string, error)
	// GenAiRerank 文档重排,返回按相关度排序的下标与得分。
	GenAiRerank(ctx context.Context, cred Credentials, region, modelOcid, query string, documents []string, topN *int) ([]RerankRank, error)
	// GenAiApplyGuardrails 对单条文本执行内容审核 / PII / 提示注入检测。
	GenAiApplyGuardrails(ctx context.Context, cred Credentials, region, text string) (*GuardrailsOutcome, error)
	// 控制台连接：创建（VNC/串口连接串）、列出、删除。
	CreateConsoleConnection(ctx context.Context, cred Credentials, region, instanceID, sshPublicKey string) (ConsoleConnection, error)
	ListConsoleConnections(ctx context.Context, cred Credentials, region, instanceID string) ([]ConsoleConnection, error)
	DeleteConsoleConnection(ctx context.Context, cred Credentials, region, connectionID string) error
	// 引导卷挂载：列出、挂载、分离、替换。
	ListBootVolumeAttachments(ctx context.Context, cred Credentials, region, instanceID string) ([]BootVolumeAttachment, error)
	AttachBootVolume(ctx context.Context, cred Credentials, region, instanceID, bootVolumeID string) (BootVolumeAttachment, error)
	DetachBootVolume(ctx context.Context, cred Credentials, region, attachmentID string) error
	ReplaceBootVolume(ctx context.Context, cred Credentials, region, instanceID, newBootVolumeID string) (BootVolumeAttachment, error)
	// 块卷挂载：列出块卷、列出挂载、附加、分离。
	ListBlockVolumes(ctx context.Context, cred Credentials, region, availabilityDomain string) ([]BlockVolume, error)
	ListVolumeAttachments(ctx context.Context, cred Credentials, region, instanceID string) ([]VolumeAttachment, error)
	AttachVolume(ctx context.Context, cred Credentials, region, instanceID, volumeID string, readOnly bool) (VolumeAttachment, error)
	DetachVolume(ctx context.Context, cred Credentials, region, attachmentID string) error
	// 租户观测：实例流量统计、成本分析。
	SummarizeInstanceTraffic(ctx context.Context, cred Credentials, q TrafficQuery) (InstanceTraffic, error)
	SummarizeCosts(ctx context.Context, cred Credentials, q CostQuery) ([]CostItem, error)
	// ListAuditEvents 实时查询租户根 compartment 的审计事件(最多翻 maxAuditPages 页,
	// 到限截断并回传续查游标);page 传上次结果的 NextPage 可从断点续翻。
	// 批式懒加载走 ListAuditEventsBatch,本方法保留给详情小窗反查。
	ListAuditEvents(ctx context.Context, cred Credentials, region string, start, end time.Time, page string) (AuditEventsResult, error)
	// ListAuditEventsBatch 从游标位置向更早方向收集约 limit 条保留事件,
	// 分窗回溯(空窗倍增),跨批不重不漏;到 365 天保留期尽头置 Exhausted。
	ListAuditEventsBatch(ctx context.Context, cred Credentials, region string, cur AuditCursor, limit int) (AuditBatchResult, error)
	// 身份域:租户 ACTIVE 域枚举(多域租户选择器数据源)。
	ListIdentityDomains(ctx context.Context, cred Credentials, region string) ([]IdentityDomain, error)
	// 账户能力:暂停标记/账户状态/免费层信息(对暂停租户仍可访问,测活据此判「暂停」)。
	GetAccountCapabilities(ctx context.Context, cred Credentials, region string) (AccountCapabilities, error)
	// 租户用户管理:domainID 非空按域走 Identity Domains SCIM,
	// 为空保持经典 IAM 为主、新式租户自动回退的旧行为。
	ListTenantUsers(ctx context.Context, cred Credentials, homeRegion, domainID string) ([]TenantUser, error)
	GetTenantUserDetail(ctx context.Context, cred Credentials, homeRegion, domainID, userID string) (TenantUserDetail, error)
	CreateTenantUser(ctx context.Context, cred Credentials, homeRegion, domainID string, in CreateTenantUserInput) (TenantUser, error)
	UpdateTenantUser(ctx context.Context, cred Credentials, homeRegion, domainID, userID string, in UpdateTenantUserInput) (TenantUser, error)
	DeleteTenantUser(ctx context.Context, cred Credentials, homeRegion, domainID, userID string) error
	ResetTenantUserPassword(ctx context.Context, cred Credentials, homeRegion, domainID, userID string) (string, error)
	DeleteTenantUserMfaDevices(ctx context.Context, cred Credentials, homeRegion, domainID, userID string) (int, error)
	DeleteTenantUserApiKeys(ctx context.Context, cred Credentials, homeRegion, userID string, includeCurrent bool) (int, error)
	// 域设置：通知收件人、密码策略、身份设置。
	GetNotificationRecipients(ctx context.Context, cred Credentials, region, domainID string) (NotificationRecipients, error)
	UpdateNotificationRecipients(ctx context.Context, cred Credentials, region, domainID string, emails []string) (NotificationRecipients, error)
	ListPasswordPolicies(ctx context.Context, cred Credentials, region, domainID string) ([]PasswordPolicyInfo, error)
	UpdatePasswordPolicy(ctx context.Context, cred Credentials, region, domainID, policyID string, in UpdatePasswordPolicyInput) (PasswordPolicyInfo, error)
	GetIdentitySetting(ctx context.Context, cred Credentials, region, domainID string) (IdentitySettingInfo, error)
	UpdateIdentitySetting(ctx context.Context, cred Credentials, region, domainID string, primaryEmailRequired bool) (IdentitySettingInfo, error)
	// Federation：SAML IdP 管理、域 SP 元数据、sign-on 免 MFA 规则。
	ListIdentityProviders(ctx context.Context, cred Credentials, region, domainID string) ([]IdentityProviderInfo, error)
	CreateSamlIdentityProvider(ctx context.Context, cred Credentials, region, domainID string, in CreateIdpInput) (IdentityProviderInfo, error)
	SetIdentityProviderEnabled(ctx context.Context, cred Credentials, region, domainID, idpID string, enabled bool) (IdentityProviderInfo, error)
	DeleteIdentityProvider(ctx context.Context, cred Credentials, region, domainID, idpID string) error
	DownloadDomainSamlMetadata(ctx context.Context, cred Credentials, region, domainID string) ([]byte, error)
	ListConsoleSignOnRules(ctx context.Context, cred Credentials, region, domainID string) ([]SignOnRuleInfo, error)
	CreateMfaExemptionRule(ctx context.Context, cred Credentials, region, domainID, idpID, ruleName string) (SignOnRuleInfo, error)
	DeleteMfaExemptionRule(ctx context.Context, cred Credentials, region, domainID, ruleID string) error
	// 日志回传链路(方案A):Topic/CUSTOM_HTTPS 订阅/IAM Policy/Service Connector
	// 的幂等创建、状态聚合与销毁;IAM 写操作发往 homeRegion。
	EnsureRelayTopic(ctx context.Context, cred Credentials) (RelayResource, error)
	EnsureRelaySubscription(ctx context.Context, cred Credentials, topicID, endpoint string) (RelayResource, error)
	GetRelaySubscription(ctx context.Context, cred Credentials, subscriptionID string) (RelayResource, error)
	EnsureRelayPolicy(ctx context.Context, cred Credentials, homeRegion string) (RelayResource, error)
	EnsureRelayConnector(ctx context.Context, cred Credentials, topicID, condition string) (RelayResource, error)
	RelayState(ctx context.Context, cred Credentials, endpoint string) (RelayState, error)
	DeleteRelayConnector(ctx context.Context, cred Credentials, connectorID string) error
	DeleteRelayPolicy(ctx context.Context, cred Credentials, homeRegion, policyID string) error
	DeleteRelaySubscription(ctx context.Context, cred Credentials, subscriptionID string) error
	DeleteRelayTopic(ctx context.Context, cred Credentials, topicID string) error
}

// RealClient 基于官方 SDK 实现 Client。
type RealClient struct {
	limitDefs sync.Map // tenancy|region|service → limitDefEntry，配额定义预筛缓存
	shapes    sync.Map // tenancy|region|ad → shapeCacheEntry，shape 清单缓存
}

// NewClient 返回生产使用的真实客户端。
func NewClient() *RealClient { return &RealClient{} }

func provider(cred Credentials) common.ConfigurationProvider {
	var pass *string
	if cred.Passphrase != "" {
		pass = &cred.Passphrase
	}
	return common.NewRawConfigurationProvider(
		cred.TenancyOCID, cred.UserOCID, cred.Region,
		cred.Fingerprint, cred.PrivateKey, pass,
	)
}

// normalizeRegion 兼容区域短名：iad → us-ashburn-1；空值与全名原样返回。
// 实例等资源对象上的 region 字段是短名，直接透传时借此归一。
func normalizeRegion(region string) string {
	if region == "" {
		return ""
	}
	return string(common.StringToRegion(region))
}

// ValidateKey 实现 Client。
func (c *RealClient) ValidateKey(ctx context.Context, cred Credentials) (TenancyInfo, error) {
	ic, err := identity.NewIdentityClientWithConfigurationProvider(provider(cred))
	if err != nil {
		return TenancyInfo{}, fmt.Errorf("new identity client: %w", err)
	}
	applyProxy(&ic.BaseClient, cred)
	resp, err := ic.GetTenancy(ctx, identity.GetTenancyRequest{TenancyId: &cred.TenancyOCID})
	if err != nil {
		return TenancyInfo{}, fmt.Errorf("get tenancy: %w", err)
	}
	return TenancyInfo{
		Name:          deref(resp.Name),
		HomeRegionKey: deref(resp.HomeRegionKey),
	}, nil
}

// FetchAccountProfile 实现 Client：从订阅列表选中 CLOUDCM，
// 再查询完整订阅并按付费模式、促销状态判定账户类别。
func (c *RealClient) FetchAccountProfile(ctx context.Context, cred Credentials) (AccountProfile, error) {
	subs, err := c.ListSubscriptions(ctx, cred)
	if err != nil {
		return AccountProfile{}, err
	}
	subID := ""
	for _, sub := range subs {
		if strings.EqualFold(sub.ServiceName, cloudcmServiceName) {
			subID = sub.ID
			break
		}
	}
	if subID == "" {
		return AccountProfile{}, ErrNoCloudcmSubscription
	}
	detail, err := c.GetSubscription(ctx, cred, subID)
	if err != nil {
		return AccountProfile{}, err
	}
	return buildAccountProfile(detail), nil
}

func buildAccountProfile(detail SubscriptionDetail) AccountProfile {
	p := AccountProfile{
		SubscriptionID:   detail.ID,
		ServiceName:      detail.ServiceName,
		PaymentModel:     detail.PaymentModel,
		SubscriptionTier: detail.SubscriptionTier,
	}
	p.PromotionStatus, p.PromotionAmount, p.PromotionExpires = pickPromotion(detail.Promotions)
	p.AccountType = ClassifyAccount(p.PaymentModel, p.SubscriptionTier, p.PromotionStatus)
	return p
}

// pickPromotion 从多条促销记录中选出最能代表当前状态的一条：
// 优先 ACTIVE，其次 EXPIRED，否则回退第一条。
func pickPromotion(promos []PromotionInfo) (string, float32, *time.Time) {
	best, bestRank := -1, -1
	for i, p := range promos {
		if rank := promotionRank(p.Status); rank > bestRank {
			best, bestRank = i, rank
		}
	}
	if best < 0 {
		return "", 0, nil
	}
	p := promos[best]
	return p.Status, p.Amount, p.TimeExpired
}

func promotionRank(status string) int {
	switch strings.ToUpper(status) {
	case "ACTIVE":
		return 2
	case "EXPIRED":
		return 1
	default:
		return 0
	}
}

func deref(s *string) string {
	if s != nil {
		return *s
	}
	return ""
}

// orEmpty 把 nil 切片归一为空切片：JSON 序列化输出 [] 而非 null，
// 避免前端对 null 调用 .length / .map / .join 崩溃。
func orEmpty[T any](s []T) []T {
	if s == nil {
		return []T{}
	}
	return s
}
