package usecase

import (
	"context"
	"encoding/json"
	"errors"
	"strings"

	"haixun-backend/internal/library/clock"
	app "haixun-backend/internal/library/errors"
	"haixun-backend/internal/library/errors/code"
	"haixun-backend/internal/library/placement"
	memberdomain "haixun-backend/internal/model/member/domain/usecase"
	personadomain "haixun-backend/internal/model/persona/domain/usecase"
	settingdomain "haixun-backend/internal/model/setting/domain/usecase"
	"haixun-backend/internal/model/threads_account/domain/entity"
	domrepo "haixun-backend/internal/model/threads_account/domain/repository"
	domusecase "haixun-backend/internal/model/threads_account/domain/usecase"

	"github.com/google/uuid"
)

const (
	settingScopeUser        = "user"
	settingScopeAccount     = "account"
	keyConnectionPrefs      = "connection.prefs"
	defaultSearchSourceMode = "mixed"
	defaultRepliesPerPost   = 10
)

type threadsAccountUseCase struct {
	repo        domrepo.Repository
	secretsRepo domrepo.SecretsRepository
	members     memberdomain.UseCase
	settings    settingdomain.UseCase
	personas    personadomain.UseCase
}

func NewUseCase(
	repo domrepo.Repository,
	secretsRepo domrepo.SecretsRepository,
	members memberdomain.UseCase,
	settings settingdomain.UseCase,
	personas personadomain.UseCase,
) domusecase.UseCase {
	return &threadsAccountUseCase{
		repo:        repo,
		secretsRepo: secretsRepo,
		members:     members,
		settings:    settings,
		personas:    personas,
	}
}

func (u *threadsAccountUseCase) List(ctx context.Context, tenantID, ownerUID string) (*domusecase.ListResult, error) {
	if err := requireActor(tenantID, ownerUID); err != nil {
		return nil, err
	}
	items, err := u.repo.ListByOwner(ctx, tenantID, ownerUID)
	if err != nil {
		return nil, err
	}
	member, err := u.members.GetByUID(ctx, tenantID, ownerUID)
	if err != nil {
		return nil, err
	}
	activeID := member.ActiveThreadsAccountID
	if activeID == "" && len(items) > 0 {
		activeID = items[0].ID
	}
	list := make([]domusecase.AccountSummary, 0, len(items))
	for _, item := range items {
		summary, err := u.toSummary(ctx, item)
		if err != nil {
			return nil, err
		}
		list = append(list, *summary)
	}
	return &domusecase.ListResult{List: list, ActiveAccountID: activeID}, nil
}

func (u *threadsAccountUseCase) Create(ctx context.Context, req domusecase.CreateRequest) (*domusecase.AccountSummary, error) {
	if err := requireActor(req.TenantID, req.OwnerUID); err != nil {
		return nil, err
	}
	displayName := strings.TrimSpace(req.DisplayName)
	if displayName == "" {
		existing, err := u.repo.ListByOwner(ctx, req.TenantID, req.OwnerUID)
		if err != nil {
			return nil, err
		}
		displayName = "帳號 " + itoa(len(existing)+1)
	}
	account, err := u.repo.Create(ctx, &entity.Account{
		ID:          uuid.NewString(),
		TenantID:    req.TenantID,
		OwnerUID:    req.OwnerUID,
		DisplayName: displayName,
		Status:      entity.StatusOpen,
	})
	if err != nil {
		return nil, err
	}
	if req.Activate {
		if err := u.members.SetActiveThreadsAccountID(ctx, req.TenantID, req.OwnerUID, account.ID); err != nil {
			return nil, err
		}
	}
	return u.toSummary(ctx, account)
}

func (u *threadsAccountUseCase) Get(ctx context.Context, tenantID, ownerUID, accountID string) (*domusecase.AccountSummary, error) {
	account, err := u.assertOwned(ctx, tenantID, ownerUID, accountID)
	if err != nil {
		return nil, err
	}
	return u.toSummary(ctx, account)
}

func (u *threadsAccountUseCase) Update(ctx context.Context, req domusecase.UpdateAccountRequest) (*domusecase.AccountSummary, error) {
	if _, err := u.assertOwned(ctx, req.TenantID, req.OwnerUID, req.AccountID); err != nil {
		return nil, err
	}
	var personaID *string
	if req.PersonaID != nil {
		trimmed := strings.TrimSpace(*req.PersonaID)
		if trimmed == "" {
			empty := ""
			personaID = &empty
		} else {
			if _, err := u.personas.Get(ctx, req.TenantID, req.OwnerUID, trimmed); err != nil {
				return nil, err
			}
			personaID = &trimmed
		}
	}
	account, err := u.repo.UpdateShell(ctx, req.TenantID, req.OwnerUID, req.AccountID, req.DisplayName, nil, personaID)
	if err != nil {
		return nil, err
	}
	return u.toSummary(ctx, account)
}

func (u *threadsAccountUseCase) Activate(ctx context.Context, tenantID, ownerUID, accountID string) error {
	if _, err := u.assertOwned(ctx, tenantID, ownerUID, accountID); err != nil {
		return err
	}
	return u.members.SetActiveThreadsAccountID(ctx, tenantID, ownerUID, accountID)
}

func (u *threadsAccountUseCase) GetConnection(ctx context.Context, tenantID, ownerUID, accountID string) (*domusecase.ConnectionData, error) {
	account, err := u.assertOwned(ctx, tenantID, ownerUID, accountID)
	if err != nil {
		return nil, err
	}
	prefs, err := u.loadConnectionPrefs(ctx, accountID)
	if err != nil {
		return nil, err
	}
	browserConnected, apiConnected, err := u.connectionFlags(ctx, accountID)
	if err != nil {
		return nil, err
	}
	return &domusecase.ConnectionData{
		AccountID:        account.ID,
		AccountName:      accountLabel(account),
		Username:         account.Username,
		BrowserConnected: browserConnected,
		ApiConnected:     apiConnected,
		Prefs:            prefs,
	}, nil
}

func (u *threadsAccountUseCase) UpdateConnection(ctx context.Context, req domusecase.UpdateConnectionRequest) (*domusecase.ConnectionData, error) {
	account, err := u.assertOwned(ctx, req.TenantID, req.OwnerUID, req.AccountID)
	if err != nil {
		return nil, err
	}
	current, err := u.loadConnectionPrefs(ctx, req.AccountID)
	if err != nil {
		return nil, err
	}
	next := applyConnectionPatch(current, req.Prefs)
	if err := u.saveConnectionPrefs(ctx, req.AccountID, next); err != nil {
		return nil, err
	}
	browserConnected, apiConnected, err := u.connectionFlags(ctx, req.AccountID)
	if err != nil {
		return nil, err
	}
	return &domusecase.ConnectionData{
		AccountID:        account.ID,
		AccountName:      accountLabel(account),
		Username:         account.Username,
		BrowserConnected: browserConnected,
		ApiConnected:     apiConnected,
		Prefs:            next,
	}, nil
}

func (u *threadsAccountUseCase) ImportBrowserSession(ctx context.Context, req domusecase.ImportBrowserSessionRequest) (*domusecase.ImportBrowserSessionResult, error) {
	account, err := u.assertOwned(ctx, req.TenantID, req.OwnerUID, req.AccountID)
	if err != nil {
		return nil, err
	}
	normalized, err := normalizeStorageState(req.StorageState)
	if err != nil {
		return nil, err
	}
	secrets, err := u.secretsRepo.SaveBrowserStorageState(ctx, account.ID, normalized)
	if err != nil {
		return nil, err
	}
	message := "Chrome session 已同步到開發模式爬蟲"
	if account.Username != "" {
		message = "Chrome session 已同步：@" + account.Username
	}
	return &domusecase.ImportBrowserSessionResult{
		AccountID: account.ID,
		Username:  account.Username,
		Synced:    true,
		Valid:     true,
		Message:   message,
		UpdateAt:  secrets.UpdateAt,
	}, nil
}

func (u *threadsAccountUseCase) GetBrowserSession(ctx context.Context, tenantID, ownerUID, accountID string) (*domusecase.BrowserSessionData, error) {
	account, err := u.assertOwned(ctx, tenantID, ownerUID, accountID)
	if err != nil {
		return nil, err
	}
	secrets, err := u.secretsRepo.FindByAccountID(ctx, account.ID)
	if err != nil {
		return nil, err
	}
	if secrets == nil || strings.TrimSpace(secrets.BrowserStorageState) == "" {
		return nil, app.For(code.ThreadsAccount).ResNotFound("browser session not synced")
	}
	return &domusecase.BrowserSessionData{
		AccountID:    account.ID,
		StorageState: secrets.BrowserStorageState,
		UpdateAt:     secrets.UpdateAt,
	}, nil
}

func (u *threadsAccountUseCase) assertOwned(ctx context.Context, tenantID, ownerUID, accountID string) (*entity.Account, error) {
	if err := requireActor(tenantID, ownerUID); err != nil {
		return nil, err
	}
	if strings.TrimSpace(accountID) == "" {
		return nil, app.For(code.ThreadsAccount).InputMissingRequired("account id is required")
	}
	return u.repo.FindByID(ctx, tenantID, ownerUID, accountID)
}

func (u *threadsAccountUseCase) toSummary(ctx context.Context, account *entity.Account) (*domusecase.AccountSummary, error) {
	browserConnected, apiConnected, err := u.connectionFlags(ctx, account.ID)
	if err != nil {
		return nil, err
	}
	return &domusecase.AccountSummary{
		ID:               account.ID,
		DisplayName:      account.DisplayName,
		Username:         account.Username,
		ThreadsUserID:    account.ThreadsUserID,
		PersonaID:        account.PersonaID,
		BrowserConnected: browserConnected,
		ApiConnected:     apiConnected,
		Status:           string(account.Status),
		CreateAt:         account.CreateAt,
		UpdateAt:         account.UpdateAt,
	}, nil
}

func (u *threadsAccountUseCase) connectionFlags(ctx context.Context, accountID string) (bool, bool, error) {
	secrets, err := u.secretsRepo.FindByAccountID(ctx, accountID)
	if err != nil {
		return false, false, err
	}
	if secrets == nil {
		return false, false, nil
	}
	browserConnected := strings.TrimSpace(secrets.BrowserStorageState) != ""
	apiConnected := strings.TrimSpace(secrets.APIAccessToken) != "" &&
		(secrets.APITokenExpiresAt == 0 || secrets.APITokenExpiresAt > clock.NowUnixNano())
	return browserConnected, apiConnected, nil
}

func (u *threadsAccountUseCase) loadConnectionPrefs(ctx context.Context, accountID string) (domusecase.ConnectionPrefs, error) {
	defaults := defaultConnectionPrefs()
	setting, err := u.settings.Get(ctx, settingScopeAccount, accountID, keyConnectionPrefs)
	if err != nil {
		if isSettingNotFound(err) {
			return defaults, nil
		}
		return defaults, err
	}
	merged := mergeConnectionPrefs(defaults, setting.Value)
	return normalizeConnectionPrefs(merged), nil
}

func (u *threadsAccountUseCase) saveConnectionPrefs(ctx context.Context, accountID string, prefs domusecase.ConnectionPrefs) error {
	_, err := u.settings.Upsert(ctx, settingdomain.UpsertRequest{
		Scope:   settingScopeAccount,
		ScopeID: accountID,
		Key:     keyConnectionPrefs,
		Value:   connectionPrefsToMap(prefs),
	})
	return err
}

func requireActor(tenantID, ownerUID string) error {
	if strings.TrimSpace(tenantID) == "" || strings.TrimSpace(ownerUID) == "" {
		return app.For(code.ThreadsAccount).InputMissingRequired("tenant_id and uid are required")
	}
	return nil
}

func accountLabel(account *entity.Account) string {
	if account == nil {
		return "未命名帳號"
	}
	if name := strings.TrimSpace(account.DisplayName); name != "" {
		return name
	}
	if name := strings.TrimSpace(account.Username); name != "" {
		return "@" + strings.TrimPrefix(name, "@")
	}
	return "未命名帳號"
}

func defaultConnectionPrefs() domusecase.ConnectionPrefs {
	return deriveConnectionPrefsFromDevMode(false)
}

// deriveConnectionPrefsFromDevMode maps the single dev_mode switch to concrete routing prefs.
// dev_mode off → everything via Threads API; dev_mode on → everything via browser crawler.
func deriveConnectionPrefsFromDevMode(devMode bool) domusecase.ConnectionPrefs {
	if devMode {
		return domusecase.ConnectionPrefs{
			DevMode:          true,
			SearchViaApi:     false,
			SearchSourceMode: "browser",
			PublishViaApi:    false,
			ScrapeReplies:    true,
			RepliesPerPost:   defaultRepliesPerPost,
			PublishHeaded:    false,
			PlaywrightDebug:  false,
		}
	}
	return domusecase.ConnectionPrefs{
		DevMode:          false,
		SearchViaApi:     true,
		SearchSourceMode: string(placement.DefaultSearchSourceMode),
		PublishViaApi:    true,
		ScrapeReplies:    false,
		RepliesPerPost:   defaultRepliesPerPost,
		PublishHeaded:    false,
		PlaywrightDebug:  false,
	}
}

func applyConnectionPatch(current domusecase.ConnectionPrefs, patch domusecase.ConnectionPrefsPatch) domusecase.ConnectionPrefs {
	if patch.DevMode != nil {
		if *patch.DevMode {
			return deriveConnectionPrefsFromDevMode(true)
		}
		next := current
		next.DevMode = false
		if patch.SearchSourceMode != nil {
			next.SearchSourceMode = strings.TrimSpace(*patch.SearchSourceMode)
		}
		return normalizeConnectionPrefs(next)
	}
	// Legacy callers may still send granular fields; normalize by current dev flag.
	devMode := current.DevMode
	if patch.SearchViaApi != nil && !*patch.SearchViaApi {
		devMode = true
	}
	if patch.PublishViaApi != nil && !*patch.PublishViaApi {
		devMode = true
	}
	if patch.ScrapeReplies != nil && *patch.ScrapeReplies {
		devMode = true
	}
	if devMode {
		return deriveConnectionPrefsFromDevMode(true)
	}
	next := current
	if patch.SearchSourceMode != nil {
		next.SearchSourceMode = strings.TrimSpace(*patch.SearchSourceMode)
	}
	return normalizeConnectionPrefs(next)
}

func normalizeConnectionPrefs(prefs domusecase.ConnectionPrefs) domusecase.ConnectionPrefs {
	if prefs.DevMode {
		return deriveConnectionPrefsFromDevMode(true)
	}
	prefs.SearchViaApi = true
	prefs.PublishViaApi = true
	prefs.ScrapeReplies = false
	prefs.PublishHeaded = false
	prefs.PlaywrightDebug = false
	mode := placement.WithoutCrawler(placement.ParseSearchSourceMode(prefs.SearchSourceMode))
	prefs.SearchSourceMode = string(mode)
	return prefs
}

func mergeConnectionPrefs(defaults domusecase.ConnectionPrefs, value map[string]interface{}) domusecase.ConnectionPrefs {
	if value == nil {
		return defaults
	}
	if v, ok := value["search_via_api"].(bool); ok {
		defaults.SearchViaApi = v
	}
	if v, ok := value["search_source_mode"].(string); ok && strings.TrimSpace(v) != "" {
		defaults.SearchSourceMode = v
	}
	if v, ok := value["publish_via_api"].(bool); ok {
		defaults.PublishViaApi = v
	}
	if v, ok := value["dev_mode"].(bool); ok {
		defaults.DevMode = v
	}
	if v, ok := value["scrape_replies"].(bool); ok {
		defaults.ScrapeReplies = v
	}
	if v, ok := asInt(value["replies_per_post"]); ok {
		defaults.RepliesPerPost = v
	}
	if v, ok := value["publish_headed"].(bool); ok {
		defaults.PublishHeaded = v
	}
	if v, ok := value["playwright_debug"].(bool); ok {
		defaults.PlaywrightDebug = v
	}
	return defaults
}

func connectionPrefsToMap(prefs domusecase.ConnectionPrefs) map[string]interface{} {
	return map[string]interface{}{
		"search_via_api":     prefs.SearchViaApi,
		"search_source_mode": prefs.SearchSourceMode,
		"publish_via_api":    prefs.PublishViaApi,
		"dev_mode":           prefs.DevMode,
		"scrape_replies":     prefs.ScrapeReplies,
		"replies_per_post":   prefs.RepliesPerPost,
		"publish_headed":     prefs.PublishHeaded,
		"playwright_debug":   prefs.PlaywrightDebug,
	}
}

type playwrightStorageState struct {
	Cookies []interface{} `json:"cookies"`
	Origins []interface{} `json:"origins,omitempty"`
}

func normalizeStorageState(input string) (string, error) {
	trimmed := strings.TrimSpace(input)
	if trimmed == "" {
		return "", app.For(code.ThreadsAccount).InputMissingRequired("storageState is required")
	}
	var parsed playwrightStorageState
	if err := json.Unmarshal([]byte(trimmed), &parsed); err != nil {
		return "", app.For(code.ThreadsAccount).InputInvalidFormat("storageState is not valid JSON")
	}
	if parsed.Cookies == nil {
		return "", app.For(code.ThreadsAccount).InputInvalidFormat("storageState must include cookies array")
	}
	raw, err := json.Marshal(parsed)
	if err != nil {
		return "", err
	}
	return string(raw), nil
}

func isSettingNotFound(err error) bool {
	var appErr *app.Error
	if errors.As(err, &appErr) {
		return appErr.Category() == code.ResNotFound
	}
	return false
}

func asInt(v interface{}) (int, bool) {
	switch n := v.(type) {
	case int:
		return n, true
	case int32:
		return int(n), true
	case int64:
		return int(n), true
	case float64:
		return int(n), true
	default:
		return 0, false
	}
}

func itoa(n int) string {
	if n <= 0 {
		return "1"
	}
	buf := make([]byte, 0, 12)
	for n > 0 {
		buf = append(buf, byte('0'+n%10))
		n /= 10
	}
	for i, j := 0, len(buf)-1; i < j; i, j = i+1, j-1 {
		buf[i], buf[j] = buf[j], buf[i]
	}
	return string(buf)
}
