package identity

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

	"github.com/tidbcloud/AgentTeam/internal/core/authidentity"
	"github.com/tidbcloud/AgentTeam/internal/core/identifiers"
	"github.com/tidbcloud/AgentTeam/internal/core/secrets"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

// MiniappPairingTTL is how long a miniapp pairing code remains usable
// after it is issued. Window must be long enough to scan + complete the
// exchange round trip, short enough to limit the brute-force window.
const MiniappPairingTTL = 10 * time.Minute

// maxActiveMiniappSessions bounds account-level session management reads. A
// new device pairing replaces the oldest active family after this limit.
const maxActiveMiniappSessions = 20

// miniappPairingTokenLength is the entropy length (in bytes) of the access
// and refresh tokens minted during exchange. Mirrors the regular sign-in
// flow so miniapp sessions are indistinguishable from browser sessions.
const miniappPairingTokenLength = 32

// MiniappPairingIssueResult is what CreateMiniappPairingCode hands back to
// the API layer: the plain (unhashed) code goes into the QR payload, the
// expiry goes into the dialog so the web can refresh before it lapses.
type MiniappPairingIssueResult struct {
	Code      string
	ExpiresAt time.Time
}

// MiniappPairingExchangeResult is what ExchangeMiniappPairingCodeForSession
// hands to the auth handler. The plain access/refresh tokens are minted
// inside the same transaction that consumes the code, so a mint failure
// rolls back consumption and the code stays retryable.
type MiniappPairingExchangeResult struct {
	User          model.User
	WorkspaceID   string
	WorkspaceName string
	ServerURL     string
	AccessToken   string
	RefreshToken  string
}

// CreateMiniappPairingCode issues a one-time pairing code bound to the
// (userID, workspaceID) pair. The caller must have already authorized the
// user against the workspace (e.g. via RequireRouteWorkspace middleware).
func (store *Store) CreateMiniappPairingCode(ctx context.Context, userID string, workspaceID string, serverURL string) (MiniappPairingIssueResult, error) {
	trimmedUserID := strings.TrimSpace(userID)
	trimmedWorkspaceID := strings.TrimSpace(workspaceID)
	trimmedServerURL := strings.TrimSpace(serverURL)
	if trimmedUserID == "" {
		return MiniappPairingIssueResult{}, storeerr.BadRequest("userID is required")
	}
	if trimmedWorkspaceID == "" {
		return MiniappPairingIssueResult{}, storeerr.BadRequest("workspaceID is required")
	}
	if trimmedServerURL == "" {
		return MiniappPairingIssueResult{}, storeerr.BadRequest("serverURL is required")
	}

	pairingID, err := identifiers.NewEntityID("mpc")
	if err != nil {
		return MiniappPairingIssueResult{}, fmt.Errorf("generate miniapp pairing id: %w", err)
	}
	plainCode, err := secrets.NewToken(32)
	if err != nil {
		return MiniappPairingIssueResult{}, fmt.Errorf("generate miniapp pairing code: %w", err)
	}
	hashedCode := secrets.HashToken(plainCode)

	now := time.Now().UTC()
	expiresAt := now.Add(MiniappPairingTTL)
	if _, err := store.db.Exec(ctx, `
INSERT INTO miniapp_pairing_codes (id, workspace_id, user_id, token_hash, server_url, expires_at, created_at)
VALUES ($1, $2, $3, $4, $5, $6, $7)
`, pairingID, trimmedWorkspaceID, trimmedUserID, hashedCode, trimmedServerURL, expiresAt, now); err != nil {
		return MiniappPairingIssueResult{}, fmt.Errorf("insert miniapp pairing code: %w", err)
	}

	return MiniappPairingIssueResult{Code: plainCode, ExpiresAt: expiresAt}, nil
}

// ExchangeMiniappPairingCodeForSession consumes a one-time pairing code and
// mints a fresh session + refresh token in the SAME transaction. Single-use
// is enforced by FOR UPDATE + guarded UPDATE: a concurrent exchanger that
// arrives while the first tx holds the row lock blocks, then re-reads after
// commit and sees used_at IS NOT NULL → Gone.
//
// Pre-generating the plain tokens outside the tx and only writing their
// hashes inside means a mint-side failure (db write error, etc.) aborts the
// whole transaction: no session/refresh row, no used_at update. The same
// code can be exchanged again on retry.
//
// EnsureDefaultWorkspace is intentionally skipped: only an existing workspace
// member could have issued the code in the first place, so the user already
// has a default workspace.
func (store *Store) ExchangeMiniappPairingCodeForSession(ctx context.Context, code string, client authidentity.MiniappPairingClient, sessionTTL time.Duration) (MiniappPairingExchangeResult, error) {
	trimmedCode := strings.TrimSpace(code)
	if trimmedCode == "" {
		return MiniappPairingExchangeResult{}, storeerr.BadRequest("code is required")
	}
	if sessionTTL <= 0 {
		return MiniappPairingExchangeResult{}, fmt.Errorf("sessionTTL must be positive")
	}
	installationID := strings.TrimSpace(client.InstallationID)
	if installationID == "" || len(installationID) > 255 {
		return MiniappPairingExchangeResult{}, storeerr.BadRequest("installationId is required")
	}
	deviceName := strings.TrimSpace(client.DeviceName)
	if deviceName == "" {
		deviceName = "Mobile device"
	}
	if len(deviceName) > 255 {
		return MiniappPairingExchangeResult{}, storeerr.BadRequest("deviceName is too long")
	}
	devicePlatform := strings.ToLower(strings.TrimSpace(client.DevicePlatform))
	switch devicePlatform {
	case "ios", "android", "devtools", "ohos":
	default:
		devicePlatform = "unknown"
	}
	client = authidentity.MiniappPairingClient{
		InstallationID: installationID,
		DeviceName:     deviceName,
		DevicePlatform: devicePlatform,
	}

	codeHash := secrets.HashToken(trimmedCode)

	accessToken, err := secrets.NewToken(miniappPairingTokenLength)
	if err != nil {
		return MiniappPairingExchangeResult{}, fmt.Errorf("generate access token: %w", err)
	}
	refreshToken, err := secrets.NewToken(miniappPairingTokenLength)
	if err != nil {
		return MiniappPairingExchangeResult{}, fmt.Errorf("generate refresh token: %w", err)
	}
	accessTokenHash := secrets.HashToken(accessToken)
	refreshTokenHash := secrets.HashToken(refreshToken)

	now := time.Now().UTC()
	sessionExpiresAt := now.Add(sessionTTL)

	return withTxValue(ctx, store.db, func(tx coredb.Tx) (MiniappPairingExchangeResult, error) {
		return store.exchangeMiniappPairingCodeTx(
			ctx, tx, codeHash, client, accessToken, refreshToken,
			accessTokenHash, refreshTokenHash, sessionExpiresAt, now,
		)
	})
}

func (store *Store) exchangeMiniappPairingCodeTx(
	ctx context.Context,
	tx coredb.Tx,
	codeHash string,
	client authidentity.MiniappPairingClient,
	accessToken string,
	refreshToken string,
	accessTokenHash string,
	refreshTokenHash string,
	sessionExpiresAt time.Time,
	now time.Time,
) (MiniappPairingExchangeResult, error) {
	var (
		userID      string
		workspaceID string
		serverURL   string
	)
	err := tx.QueryRow(ctx, `
SELECT user_id, workspace_id, server_url
FROM miniapp_pairing_codes
WHERE token_hash = $1
  AND used_at IS NULL
  AND expires_at > $2
FOR UPDATE
`, codeHash, now).Scan(&userID, &workspaceID, &serverURL)
	if err != nil {
		if coredb.IsNotFound(err) {
			return MiniappPairingExchangeResult{}, storeerr.Gone("miniapp pairing code expired or already used")
		}
		return MiniappPairingExchangeResult{}, fmt.Errorf("load miniapp pairing code: %w", err)
	}

	// Serialize pairings for one account so concurrent QR exchanges cannot
	// exceed the product limit or create two active families for one
	// installation. The lock is held only for this explicit pairing action.
	var lockedUserID string
	if err := tx.QueryRow(ctx, `
SELECT id
FROM users
WHERE id = $1
FOR UPDATE
`, userID).Scan(&lockedUserID); err != nil {
		if coredb.IsNotFound(err) {
			return MiniappPairingExchangeResult{}, storeerr.Gone("user for miniapp pairing code no longer exists")
		}
		return MiniappPairingExchangeResult{}, fmt.Errorf("lock user for miniapp pairing: %w", err)
	}

	var workspaceName string
	if err := tx.QueryRow(ctx, `SELECT name FROM workspaces WHERE id = $1`, workspaceID).Scan(&workspaceName); err != nil {
		if coredb.IsNotFound(err) {
			return MiniappPairingExchangeResult{}, storeerr.Gone("workspace for miniapp pairing code no longer exists")
		}
		return MiniappPairingExchangeResult{}, fmt.Errorf("load workspace name for miniapp pairing code: %w", err)
	}

	user, err := loadUserWithoutPasswordHash(ctx, tx, userID)
	if err != nil {
		if coredb.IsNotFound(err) {
			return MiniappPairingExchangeResult{}, storeerr.Gone("user for miniapp pairing code no longer exists")
		}
		return MiniappPairingExchangeResult{}, fmt.Errorf("load user for miniapp pairing code: %w", err)
	}

	sessionFamilyID, err := createMiniappSessionFamily(ctx, tx, userID, client, sessionExpiresAt, now)
	if err != nil {
		return MiniappPairingExchangeResult{}, err
	}
	if err := insertMiniappSessionTokens(
		ctx, tx, userID, sessionFamilyID, accessTokenHash, refreshTokenHash, sessionExpiresAt, now,
	); err != nil {
		return MiniappPairingExchangeResult{}, err
	}

	commandTag, err := tx.Exec(ctx, `
UPDATE miniapp_pairing_codes
SET used_at = $2
WHERE token_hash = $1
  AND used_at IS NULL
  AND expires_at > $2
`, codeHash, now)
	if err != nil {
		return MiniappPairingExchangeResult{}, fmt.Errorf("consume miniapp pairing code: %w", err)
	}
	if commandTag.RowsAffected() == 0 {
		return MiniappPairingExchangeResult{}, storeerr.Gone("miniapp pairing code expired or already used")
	}

	return MiniappPairingExchangeResult{
		User:          user,
		WorkspaceID:   workspaceID,
		WorkspaceName: workspaceName,
		ServerURL:     serverURL,
		AccessToken:   accessToken,
		RefreshToken:  refreshToken,
	}, nil
}

func createMiniappSessionFamily(
	ctx context.Context,
	tx coredb.Tx,
	userID string,
	client authidentity.MiniappPairingClient,
	sessionExpiresAt time.Time,
	now time.Time,
) (string, error) {
	// A new pairing from the same app installation replaces its previous
	// login instead of leaving an invisible credential family behind.
	if _, err := tx.Exec(ctx, `
UPDATE user_session_families
SET revoked_at = $3
WHERE user_id = $1
  AND client_type = 'miniapp'
  AND installation_id = $2
  AND revoked_at IS NULL
  AND expires_at > $3
`, userID, client.InstallationID, now); err != nil {
		return "", fmt.Errorf("revoke previous miniapp session family: %w", err)
	}

	rows, err := tx.Query(ctx, `
SELECT id
FROM user_session_families
WHERE user_id = $1
  AND client_type = 'miniapp'
  AND revoked_at IS NULL
  AND expires_at > $2
ORDER BY created_at DESC, id DESC
LIMIT 20
FOR UPDATE
`, userID, now)
	if err != nil {
		return "", fmt.Errorf("load active miniapp session families: %w", err)
	}
	activeFamilyIDs := make([]string, 0, maxActiveMiniappSessions)
	for rows.Next() {
		var activeFamilyID string
		if err := rows.Scan(&activeFamilyID); err != nil {
			rows.Close()
			return "", fmt.Errorf("scan active miniapp session family: %w", err)
		}
		activeFamilyIDs = append(activeFamilyIDs, activeFamilyID)
	}
	if err := rows.Err(); err != nil {
		rows.Close()
		return "", fmt.Errorf("iterate active miniapp session families: %w", err)
	}
	rows.Close()
	if len(activeFamilyIDs) == maxActiveMiniappSessions {
		if _, err := tx.Exec(ctx, `
UPDATE user_session_families
SET revoked_at = $2
WHERE id = $1 AND revoked_at IS NULL
`, activeFamilyIDs[len(activeFamilyIDs)-1], now); err != nil {
			return "", fmt.Errorf("revoke oldest miniapp session family: %w", err)
		}
	}

	sessionFamilyID, err := identifiers.NewEntityID("msf")
	if err != nil {
		return "", fmt.Errorf("generate miniapp session family id: %w", err)
	}
	if _, err := tx.Exec(ctx, `
INSERT INTO user_session_families (
  id, user_id, client_type, installation_id, device_name, device_platform,
  expires_at, created_at
)
VALUES ($1, $2, 'miniapp', $3, $4, $5, $6, $7)
`, sessionFamilyID, userID, client.InstallationID, client.DeviceName, client.DevicePlatform, sessionExpiresAt, now); err != nil {
		return "", fmt.Errorf("insert miniapp session family: %w", err)
	}
	return sessionFamilyID, nil
}

func insertMiniappSessionTokens(
	ctx context.Context,
	tx coredb.Tx,
	userID string,
	sessionFamilyID string,
	accessTokenHash string,
	refreshTokenHash string,
	sessionExpiresAt time.Time,
	now time.Time,
) error {
	sessionID, err := identifiers.NewEntityID("sess")
	if err != nil {
		return fmt.Errorf("generate session id: %w", err)
	}
	if _, err := tx.Exec(ctx, `
INSERT INTO user_sessions (id, user_id, session_family_id, token_hash, expires_at, created_at)
VALUES ($1, $2, $3, $4, $5, $6)
`, sessionID, userID, sessionFamilyID, accessTokenHash, sessionExpiresAt, now); err != nil {
		return fmt.Errorf("insert miniapp pairing session: %w", err)
	}

	refreshTokenID, err := identifiers.NewEntityID("rft")
	if err != nil {
		return fmt.Errorf("generate refresh token id: %w", err)
	}
	if _, err := tx.Exec(ctx, `
INSERT INTO user_refresh_tokens (id, user_id, session_family_id, token_hash, expires_at, created_at)
VALUES ($1, $2, $3, $4, $5, $6)
`, refreshTokenID, userID, sessionFamilyID, refreshTokenHash, sessionExpiresAt, now); err != nil {
		return fmt.Errorf("insert miniapp pairing refresh token: %w", err)
	}
	return nil
}
