package runner

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"net/http"
	"net/url"
	"regexp"
	"strings"

	"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
	"github.com/easyai/easyai-ai-gateway/apps/api/internal/clients"
	"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)

var portraitAssetPlaceholderPattern = regexp.MustCompile(`(?i)<<<[[:space:]]*portrait[_-]?asset_([0-9]+)[[:space:]]*>>>|@portrait_asset([0-9]+)|@人像资产([0-9]+)`)

type PortraitAssetCapability struct {
	Enabled                  bool     `json:"enabled"`
	CanUse                   bool     `json:"canUse"`
	CanCreate                bool     `json:"canCreate"`
	CanUseAsPortraitAsset    bool     `json:"canUseAsPortraitAsset"`
	CanUseAsPlainMaterial    bool     `json:"canUseAsPlainMaterial"`
	AvailablePlatformIDs     []string `json:"availablePlatformIds"`
	CreationPlatformIDs      []string `json:"creationPlatformIds"`
	CanReferenceTencentAsset bool     `json:"canReferenceTencentAssetUri"`
	Reason                   string   `json:"reason,omitempty"`
}

type PortraitAssetCreateInput struct {
	Name                  string
	Description           string
	SourceType            string
	URL                   string
	Preview               string
	MimeType              string
	ByteSize              int64
	SourceSHA256          string
	PrivateAvatarEligible bool
	Metadata              map[string]any
}

type PortraitAssetSyncResponse struct {
	Requested int                   `json:"requested"`
	Accepted  int                   `json:"accepted"`
	SyncedIDs []string              `json:"syncedIds"`
	Skipped   []PortraitAssetIssue  `json:"skipped"`
	Failed    []PortraitAssetIssue  `json:"failed"`
	Assets    []store.PortraitAsset `json:"assets"`
}

type PortraitAssetIssue struct {
	ID     string `json:"id"`
	Reason string `json:"reason"`
}

type portraitAssetPlatformSettings struct {
	ProjectName  string
	AssetGroupID string
	Credentials  clients.VolcesAssetCredentials
}

func (s *Service) PortraitAssetCapability(ctx context.Context) (PortraitAssetCapability, error) {
	platforms, err := s.store.ListPortraitAssetPlatforms(ctx)
	if err != nil {
		return PortraitAssetCapability{}, err
	}
	ids := make([]string, 0, len(platforms))
	for _, platform := range platforms {
		if _, ok := portraitAssetSettings(platform); ok {
			ids = append(ids, platform.PlatformID)
		}
	}
	capability := PortraitAssetCapability{
		Enabled:               len(ids) > 0,
		CanUse:                len(ids) > 0,
		CanCreate:             len(ids) > 0,
		CanUseAsPortraitAsset: len(ids) > 0,
		CanUseAsPlainMaterial: true,
		AvailablePlatformIDs:  ids,
		CreationPlatformIDs:   ids,
	}
	if len(ids) == 0 {
		capability.Reason = "未配置可用的火山 Seedance 人像资产平台；请在 Volces 平台 config.seedancePrivateAsset 中配置 enabled、accessKey、secretKey、projectName、assetGroupId。"
	}
	return capability, nil
}

func (s *Service) CreatePortraitAsset(ctx context.Context, user *auth.User, input PortraitAssetCreateInput) (store.PortraitAsset, bool, error) {
	if s.store == nil {
		return store.PortraitAsset{}, false, fmt.Errorf("portrait asset store is unavailable")
	}
	if !validPortraitAssetSourceType(input.SourceType) {
		return store.PortraitAsset{}, false, &clients.ClientError{Code: "portrait_asset_unsupported_type", Message: "source type must be image, video, or audio", StatusCode: http.StatusBadRequest, Retryable: false}
	}
	if strings.TrimSpace(input.URL) == "" {
		return store.PortraitAsset{}, false, &clients.ClientError{Code: "portrait_asset_source_url_required", Message: "portrait asset source URL is required", StatusCode: http.StatusBadRequest, Retryable: false}
	}
	if !input.PrivateAvatarEligible {
		return store.PortraitAsset{}, false, &clients.ClientError{Code: "portrait_asset_authorization_required", Message: "private_avatar_eligible must be true after the user confirms authorization", StatusCode: http.StatusBadRequest, Retryable: false}
	}
	if existing, found, err := s.store.FindPortraitAssetBySourceHash(ctx, user, input.SourceSHA256); err != nil {
		return store.PortraitAsset{}, false, err
	} else if found {
		return existing, true, nil
	}
	gatewayUserID, userID := portraitAssetUserKeys(user)
	if user == nil || userID == "" {
		return store.PortraitAsset{}, false, store.ErrLocalUserRequired
	}
	asset, err := s.store.CreatePortraitAsset(ctx, store.PortraitAssetInput{
		GatewayUserID:         gatewayUserID,
		UserID:                userID,
		GatewayTenantID:       strings.TrimSpace(user.GatewayTenantID),
		TenantID:              strings.TrimSpace(user.TenantID),
		TenantKey:             strings.TrimSpace(user.TenantKey),
		Name:                  strings.TrimSpace(input.Name),
		Description:           strings.TrimSpace(input.Description),
		SourceType:            strings.ToLower(strings.TrimSpace(input.SourceType)),
		URL:                   strings.TrimSpace(input.URL),
		Preview:               firstNonEmptyString(strings.TrimSpace(input.Preview), strings.TrimSpace(input.URL)),
		MimeType:              strings.TrimSpace(input.MimeType),
		ByteSize:              input.ByteSize,
		SourceSHA256:          strings.TrimSpace(input.SourceSHA256),
		PrivateAvatarEligible: input.PrivateAvatarEligible,
		Metadata:              input.Metadata,
	})
	return asset, false, err
}

func (s *Service) SyncPortraitAssets(ctx context.Context, user *auth.User, ids []string) (PortraitAssetSyncResponse, error) {
	response := PortraitAssetSyncResponse{
		Requested: len(ids), SyncedIDs: make([]string, 0), Skipped: make([]PortraitAssetIssue, 0), Failed: make([]PortraitAssetIssue, 0), Assets: make([]store.PortraitAsset, 0),
	}
	platforms, err := s.store.ListPortraitAssetPlatforms(ctx)
	if err != nil {
		return response, err
	}
	configured := make([]store.PortraitAssetPlatform, 0, len(platforms))
	for _, platform := range platforms {
		if _, ok := portraitAssetSettings(platform); ok {
			configured = append(configured, platform)
		}
	}
	seen := map[string]bool{}
	for _, value := range ids {
		assetID := strings.TrimSpace(value)
		if assetID == "" || seen[assetID] {
			continue
		}
		seen[assetID] = true
		asset, found, err := s.store.FindPortraitAssetForUser(ctx, user, assetID)
		if err != nil {
			return response, err
		}
		if !found {
			response.Skipped = append(response.Skipped, PortraitAssetIssue{ID: assetID, Reason: "portrait asset not found"})
			continue
		}
		if !asset.PrivateAvatarEligible {
			response.Skipped = append(response.Skipped, PortraitAssetIssue{ID: asset.ID, Reason: "portrait asset authorization is required"})
			continue
		}
		if len(configured) == 0 {
			_ = s.store.UpdatePortraitAssetStatus(ctx, asset.ID, "not_configured", "no configured Volces portrait asset platform")
			asset.Status = "not_configured"
			asset.LastError = "no configured Volces portrait asset platform"
			response.Skipped = append(response.Skipped, PortraitAssetIssue{ID: asset.ID, Reason: asset.LastError})
			response.Assets = append(response.Assets, asset)
			continue
		}

		response.Accepted++
		assetFailed := false
		for _, platform := range configured {
			if err := s.syncPortraitAssetToPlatform(ctx, asset, platform); err != nil {
				assetFailed = true
				response.Failed = append(response.Failed, PortraitAssetIssue{ID: asset.ID, Reason: platform.PlatformID + ": " + err.Error()})
			}
		}
		updated, _, err := s.refreshPortraitAssetStatus(ctx, user, asset.ID)
		if err != nil {
			return response, err
		}
		response.Assets = append(response.Assets, updated)
		if !assetFailed {
			response.SyncedIDs = append(response.SyncedIDs, updated.ID)
		}
	}
	return response, nil
}

func (s *Service) syncPortraitAssetToPlatform(ctx context.Context, asset store.PortraitAsset, platform store.PortraitAssetPlatform) error {
	settings, ok := portraitAssetSettings(platform)
	if !ok {
		return &clients.ClientError{Code: "portrait_asset_not_configured", Message: "platform portrait asset configuration is incomplete", Retryable: false}
	}
	binding, found, err := s.store.GetPortraitAssetBinding(ctx, asset.ID, platform.PlatformID)
	if err != nil {
		return err
	}
	if !found {
		binding = store.PortraitAssetBinding{AssetID: asset.ID, PlatformID: platform.PlatformID, ProjectName: settings.ProjectName, AssetGroupID: settings.AssetGroupID, Status: "pending"}
	}
	if !portraitAssetHasPublicURL(asset.URL) {
		return s.recordPortraitAssetBindingFailure(ctx, binding, settings, &clients.ClientError{
			Code:       "portrait_asset_public_url_required",
			Message:    "portrait asset URL must be an absolute http(s) URL reachable by Volces",
			StatusCode: http.StatusBadRequest,
			Retryable:  false,
		})
	}
	client := clients.VolcesAssetClient{HTTPClient: s.portraitAssetHTTPClient()}
	remoteID := strings.TrimSpace(binding.RemoteAssetID)
	if remoteID == "" {
		created, _, createErr := client.CreateAsset(ctx, settings.Credentials, map[string]any{
			"GroupId": settings.AssetGroupID, "URL": asset.URL, "Name": asset.Name,
			"AssetType": volcesPortraitAssetType(asset.SourceType), "ProjectName": settings.ProjectName,
		})
		if createErr != nil {
			return s.recordPortraitAssetBindingFailure(ctx, binding, settings, createErr)
		}
		remoteID = strings.TrimSpace(created.ID)
		if remoteID == "" {
			return s.recordPortraitAssetBindingFailure(ctx, binding, settings, &clients.ClientError{Code: "invalid_response", Message: "volces CreateAsset returned no asset id", Retryable: false})
		}
		binding.RemoteAssetID = remoteID
	}
	remote, _, getErr := client.GetAsset(ctx, settings.Credentials, map[string]any{"Id": remoteID, "ProjectName": settings.ProjectName})
	if getErr != nil {
		return s.recordPortraitAssetBindingFailure(ctx, binding, settings, getErr)
	}
	binding.ProjectName = settings.ProjectName
	binding.AssetGroupID = settings.AssetGroupID
	binding.RemoteAssetID = firstNonEmptyString(remote.ID, remoteID)
	binding.RemoteAssetURI = "asset://" + binding.RemoteAssetID
	binding.Status = portraitAssetBindingStatus(remote.Status)
	binding.LastErrorCode = strings.TrimSpace(stringFromMap(remote.Error, "Code"))
	binding.LastErrorMessage = strings.TrimSpace(stringFromMap(remote.Error, "Message"))
	if binding.Status == "failed" && binding.LastErrorMessage == "" {
		binding.LastErrorMessage = "volces portrait asset processing failed"
	}
	_, err = s.store.UpsertPortraitAssetBinding(ctx, binding)
	return err
}

func (s *Service) recordPortraitAssetBindingFailure(ctx context.Context, binding store.PortraitAssetBinding, settings portraitAssetPlatformSettings, cause error) error {
	binding.ProjectName = settings.ProjectName
	binding.AssetGroupID = settings.AssetGroupID
	binding.Status = "failed"
	binding.LastErrorCode = clients.ErrorCode(cause)
	binding.LastErrorMessage = cause.Error()
	_, err := s.store.UpsertPortraitAssetBinding(ctx, binding)
	if err != nil {
		return err
	}
	return cause
}

func (s *Service) refreshPortraitAssetStatus(ctx context.Context, user *auth.User, assetID string) (store.PortraitAsset, bool, error) {
	asset, found, err := s.store.FindPortraitAssetForUser(ctx, user, assetID)
	if err != nil || !found {
		return asset, found, err
	}
	active, total, lastError, _, err := s.store.PortraitAssetBindingSummary(ctx, asset.ID)
	if err != nil {
		return asset, true, err
	}
	status := "not_synced"
	if total == 0 {
		status = "not_synced"
	} else if active > 0 {
		status = "active"
		if active < total {
			status = "partial"
		}
	} else if lastError != "" {
		status = "failed"
	} else {
		status = "pending"
	}
	if err := s.store.UpdatePortraitAssetStatus(ctx, asset.ID, status, lastError); err != nil {
		return asset, true, err
	}
	asset.Status = status
	asset.LastError = lastError
	return asset, true, nil
}

func (s *Service) compilePortraitAssetReferences(ctx context.Context, user *auth.User, kind string, body map[string]any, candidate store.RuntimeModelCandidate) (map[string]any, error) {
	entries := portraitAssetList(body["portrait_asset_list"])
	if len(entries) == 0 {
		return body, nil
	}
	if kind != "videos.generations" || !isVolcesPortraitAssetCandidate(candidate) {
		return nil, &clients.ClientError{Code: "portrait_asset_unsupported_model", Message: "portrait assets require a configured Volces Seedance omni video model", StatusCode: http.StatusBadRequest, Retryable: false}
	}
	if !candidateSupportsPortraitAssets(candidate) {
		return nil, &clients.ClientError{Code: "portrait_asset_unsupported_model", Message: "selected model does not enable supports_portrait_asset_reference", StatusCode: http.StatusBadRequest, Retryable: false}
	}

	out := cloneMap(body)
	content := contentItems(out["content"])
	labels := make([]string, len(entries))
	nonAudioAssets := 0
	for index, entry := range entries {
		assetID := firstNonEmptyString(stringFromMap(entry, "id"), stringFromMap(entry, "easyai_portrait_asset_id"))
		if assetID == "" {
			return nil, &clients.ClientError{Code: "portrait_asset_id_required", Message: fmt.Sprintf("portrait_asset_list[%d].id is required", index), StatusCode: http.StatusBadRequest, Retryable: false}
		}
		asset, found, err := s.store.FindPortraitAssetForUser(ctx, user, assetID)
		if err != nil {
			return nil, err
		}
		if !found {
			return nil, &clients.ClientError{Code: "portrait_asset_not_found", Message: "portrait asset not found", StatusCode: http.StatusNotFound, Retryable: false}
		}
		if !asset.PrivateAvatarEligible {
			return nil, &clients.ClientError{Code: "portrait_asset_authorization_required", Message: "portrait asset authorization is required", StatusCode: http.StatusBadRequest, Retryable: false}
		}
		binding, bound, err := s.store.GetPortraitAssetBinding(ctx, asset.ID, candidate.PlatformID)
		if err != nil {
			return nil, err
		}
		if !bound || binding.Status != "active" || strings.TrimSpace(binding.RemoteAssetURI) == "" {
			return nil, &clients.ClientError{Code: "portrait_asset_processing", Message: "portrait asset is not active for the selected Volces platform; sync it and retry", StatusCode: http.StatusServiceUnavailable, Retryable: true}
		}
		labels[index] = firstNonEmptyString(strings.TrimSpace(stringFromMap(entry, "name")), asset.Name, "portrait asset "+fmt.Sprint(index+1))
		if asset.SourceType != "audio" {
			nonAudioAssets++
		}
		content = append(content, portraitAssetContent(asset.SourceType, binding.RemoteAssetURI))
	}
	if nonAudioAssets == 0 {
		return nil, &clients.ClientError{Code: "portrait_asset_audio_only", Message: "portrait_asset_list cannot contain audio-only assets", StatusCode: http.StatusBadRequest, Retryable: false}
	}
	for index := range content {
		if strings.ToLower(strings.TrimSpace(stringFromAny(content[index]["type"]))) != "text" {
			continue
		}
		content[index]["text"] = replacePortraitAssetPlaceholders(stringFromAny(content[index]["text"]), labels)
	}
	out["content"] = mapsToAnySlice(content)
	delete(out, "portrait_asset_list")
	return out, nil
}

func (s *Service) portraitAssetHTTPClient() *http.Client {
	if s.httpClients != nil && s.httpClients.none != nil {
		return s.httpClients.none
	}
	return http.DefaultClient
}

func portraitAssetSettings(platform store.PortraitAssetPlatform) (portraitAssetPlatformSettings, bool) {
	config := portraitAssetNestedConfig(platform.Config)
	accessKey := firstNonEmptyString(portraitAssetValue(config, "accessKey", "access_key"), portraitAssetValue(platform.Credentials, "accessKey", "access_key"))
	secretKey := firstNonEmptyString(portraitAssetValue(config, "secretKey", "secret_key"), portraitAssetValue(platform.Credentials, "secretKey", "secret_key"))
	projectName := firstNonEmptyString(portraitAssetValue(config, "projectName", "project_name"), "default")
	assetGroupID := portraitAssetValue(config, "assetGroupId", "asset_group_id")
	endpoint := firstNonEmptyString(portraitAssetValue(config, "assetEndpoint", "asset_endpoint", "volcesAssetEndpoint", "volces_asset_endpoint"), clientsVolcesAssetDefaultEndpoint())
	if accessKey == "" || secretKey == "" || projectName == "" || assetGroupID == "" {
		return portraitAssetPlatformSettings{}, false
	}
	if enabled, present := portraitAssetBool(config, "enabled"); present && !enabled {
		return portraitAssetPlatformSettings{}, false
	}
	return portraitAssetPlatformSettings{ProjectName: projectName, AssetGroupID: assetGroupID, Credentials: clients.VolcesAssetCredentials{AccessKey: accessKey, SecretKey: secretKey, Endpoint: endpoint}}, true
}

func portraitAssetNestedConfig(config map[string]any) map[string]any {
	for _, key := range []string{"seedancePrivateAsset", "seedance_private_asset", "portraitAsset", "portrait_asset"} {
		if nested, ok := config[key].(map[string]any); ok {
			return nested
		}
	}
	return config
}

func portraitAssetValue(values map[string]any, keys ...string) string {
	for _, key := range keys {
		if value := strings.TrimSpace(stringFromAny(values[key])); value != "" {
			return value
		}
	}
	return ""
}

func portraitAssetBool(values map[string]any, key string) (bool, bool) {
	value, ok := values[key]
	if !ok {
		return false, false
	}
	switch typed := value.(type) {
	case bool:
		return typed, true
	case string:
		return strings.EqualFold(strings.TrimSpace(typed), "true"), true
	default:
		return false, false
	}
}

func validPortraitAssetSourceType(value string) bool {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case "image", "video", "audio":
		return true
	default:
		return false
	}
}

func portraitAssetHasPublicURL(value string) bool {
	parsed, err := url.Parse(strings.TrimSpace(value))
	if err != nil || parsed.Host == "" {
		return false
	}
	return strings.EqualFold(parsed.Scheme, "http") || strings.EqualFold(parsed.Scheme, "https")
}

func volcesPortraitAssetType(sourceType string) string {
	switch strings.ToLower(strings.TrimSpace(sourceType)) {
	case "video":
		return "Video"
	case "audio":
		return "Audio"
	default:
		return "Image"
	}
}

func portraitAssetBindingStatus(value string) string {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case "active", "succeeded", "success":
		return "active"
	case "failed", "error":
		return "failed"
	default:
		return "processing"
	}
}

func portraitAssetList(value any) []map[string]any {
	switch typed := value.(type) {
	case []any:
		out := make([]map[string]any, 0, len(typed))
		for _, item := range typed {
			if object, ok := item.(map[string]any); ok {
				out = append(out, object)
			}
		}
		return out
	case []map[string]any:
		return typed
	default:
		return nil
	}
}

func portraitAssetContent(sourceType string, assetURI string) map[string]any {
	switch strings.ToLower(strings.TrimSpace(sourceType)) {
	case "video":
		return map[string]any{"type": "video_url", "role": "reference_video", "video_url": map[string]any{"url": assetURI}}
	case "audio":
		return map[string]any{"type": "audio_url", "role": "reference_audio", "audio_url": map[string]any{"url": assetURI}}
	default:
		return map[string]any{"type": "image_url", "role": "reference_image", "image_url": map[string]any{"url": assetURI}}
	}
}

func replacePortraitAssetPlaceholders(value string, labels []string) string {
	return portraitAssetPlaceholderPattern.ReplaceAllStringFunc(value, func(match string) string {
		parts := portraitAssetPlaceholderPattern.FindStringSubmatch(match)
		for index := 1; index < len(parts); index++ {
			if parts[index] == "" {
				continue
			}
			position := int(parts[index][0] - '0')
			if len(parts[index]) > 1 {
				position = 0
				for _, r := range parts[index] {
					position = position*10 + int(r-'0')
				}
			}
			if position > 0 && position <= len(labels) && strings.TrimSpace(labels[position-1]) != "" {
				return labels[position-1]
			}
		}
		return match
	})
}

func isVolcesPortraitAssetCandidate(candidate store.RuntimeModelCandidate) bool {
	provider := strings.ToLower(strings.TrimSpace(candidate.Provider))
	return provider == "volces" || provider == "volces-openai"
}

func candidateSupportsPortraitAssets(candidate store.RuntimeModelCandidate) bool {
	capabilities := effectiveModelCapability(candidate)
	for _, key := range []string{candidate.ModelType, "omni_video", "omni", "video_generate"} {
		if capability, ok := capabilities[key].(map[string]any); ok {
			if enabled, present := portraitAssetBool(capability, "supports_portrait_asset_reference"); present {
				return enabled
			}
		}
	}
	return false
}

func portraitAssetUserKeys(user *auth.User) (string, string) {
	if user == nil {
		return "", ""
	}
	gatewayUserID := strings.TrimSpace(user.GatewayUserID)
	if gatewayUserID == "" && user.Source == "gateway" {
		gatewayUserID = strings.TrimSpace(user.ID)
	}
	return gatewayUserID, strings.TrimSpace(user.ID)
}

func portraitAssetSHA256(payload []byte) string {
	digest := sha256.Sum256(payload)
	return hex.EncodeToString(digest[:])
}

func clientsVolcesAssetDefaultEndpoint() string { return "https://ark.cn-beijing.volcengineapi.com" }
