package agent

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

	"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/access"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

const connectTokenTTL = 5 * time.Minute

var workspaceAccessAction = access.HumanAction{
	Resource: access.ResourceWorkspace,
	Action:   access.ActionAccess,
}

// MachineConnectExchangeResult contains the machine credentials returned by a
// browser/deep-link connect exchange.
type MachineConnectExchangeResult struct {
	MachineID   string
	APIKey      string
	ServerURL   string
	WorkspaceID string
}

func (service *Service) CreateMachineConnectToken(ctx context.Context, userID string, workspaceID string, machineID string, serverURL string) (string, error) {
	if _, err := service.requireMachineManagement(ctx, userID, workspaceID, machineID); err != nil {
		return "", err
	}

	trimmedServerURL := strings.TrimSpace(serverURL)
	if trimmedServerURL == "" {
		return "", storeerr.BadRequest("serverUrl is required")
	}

	connectTokenID, err := identifiers.NewEntityID("mct")
	if err != nil {
		return "", fmt.Errorf("generate machine connect token id: %w", err)
	}
	plainToken, err := secrets.NewToken(32)
	if err != nil {
		return "", fmt.Errorf("generate machine connect token: %w", err)
	}
	hashedToken := secrets.HashToken(plainToken)

	now := time.Now().UTC()
	_, err = service.db.Exec(ctx, `
INSERT INTO machine_connect_tokens (id, machine_id, token_hash, server_url, expires_at, created_at)
VALUES ($1, $2, $3, $4, $5, $6)
`, connectTokenID, machineID, hashedToken, trimmedServerURL, now.Add(connectTokenTTL), now)
	if err != nil {
		return "", fmt.Errorf("insert machine connect token: %w", err)
	}

	return plainToken, nil
}

func (service *Service) ExchangeMachineConnectToken(ctx context.Context, token string) (MachineConnectExchangeResult, error) {
	trimmedToken := strings.TrimSpace(token)
	if trimmedToken == "" {
		return MachineConnectExchangeResult{}, storeerr.BadRequest("token is required")
	}

	var result MachineConnectExchangeResult
	tokenHash := secrets.HashToken(trimmedToken)
	now := time.Now().UTC()
	if err := coredb.WithTx(ctx, service.db, func(tx coredb.Tx) error {
		err := tx.QueryRow(ctx, `
SELECT machines.id, machines.api_key, token.server_url, machines.workspace_id
FROM machine_connect_tokens AS token
JOIN machines ON machines.id = token.machine_id
WHERE token.token_hash = $1
  AND machines.status = 'active'
  AND token.used_at IS NULL
  AND token.expires_at > $2
FOR UPDATE
`, tokenHash, now).Scan(&result.MachineID, &result.APIKey, &result.ServerURL, &result.WorkspaceID)
		if err != nil {
			if coredb.IsNotFound(err) {
				return storeerr.Gone("machine connect token expired or already used")
			}
			return fmt.Errorf("load machine connect token: %w", err)
		}

		commandTag, err := tx.Exec(ctx, `
UPDATE machine_connect_tokens
SET used_at = $2
WHERE token_hash = $1
  AND used_at IS NULL
  AND expires_at > $2
`, tokenHash, now)
		if err != nil {
			return fmt.Errorf("consume machine connect token: %w", err)
		}
		if commandTag.RowsAffected() == 0 {
			return storeerr.Gone("machine connect token expired or already used")
		}
		return nil
	}); err != nil {
		return MachineConnectExchangeResult{}, err
	}

	return result, nil
}

func (service *Service) ResolveMachineAuth(ctx context.Context, apiKey string, serverURL string) (MachineConnectExchangeResult, error) {
	trimmedAPIKey := strings.TrimSpace(apiKey)
	if trimmedAPIKey == "" {
		return MachineConnectExchangeResult{}, storeerr.BadRequest("apiKey is required")
	}
	trimmedServerURL := strings.TrimRight(strings.TrimSpace(serverURL), "/")
	if trimmedServerURL == "" {
		return MachineConnectExchangeResult{}, storeerr.BadRequest("serverUrl is required")
	}

	identity, err := service.AuthenticateMachine(ctx, trimmedAPIKey)
	if err != nil {
		if errors.Is(err, ErrUnauthorized) {
			return MachineConnectExchangeResult{}, storeerr.New(storeerr.ErrUnauthorized, "invalid machine API key")
		}
		return MachineConnectExchangeResult{}, err
	}

	return MachineConnectExchangeResult{
		MachineID:   identity.MachineID,
		APIKey:      trimmedAPIKey,
		ServerURL:   trimmedServerURL,
		WorkspaceID: identity.WorkspaceID,
	}, nil
}

func (service *Service) requireMachineManagement(ctx context.Context, userID string, workspaceID string, machineID string) (access.HumanMachineAccess, error) {
	workspaceMember, membershipErr := access.LoadAuthorizedHumanWorkspaceMember(ctx, service.db, userID, workspaceID, workspaceAccessAction)
	if errors.Is(membershipErr, access.ErrForbidden) {
		return access.HumanMachineAccess{}, storeerr.ErrForbidden
	}
	if errors.Is(membershipErr, access.ErrNotFound) {
		return access.HumanMachineAccess{}, storeerr.ErrNotFound
	}
	if membershipErr != nil {
		return access.HumanMachineAccess{}, fmt.Errorf("load authorized human workspace member: %w", membershipErr)
	}

	machine, lookupErr := access.LookupMachine(ctx, service.db, workspaceID, machineID)
	if errors.Is(lookupErr, access.ErrNotFound) {
		return access.HumanMachineAccess{}, storeerr.ErrNotFound
	}
	if lookupErr != nil {
		return access.HumanMachineAccess{}, fmt.Errorf("load runtime machine: %w", lookupErr)
	}
	createdByUserID, ownerErr := access.MachineCreatedByUser(ctx, service.db, workspaceID, machineID)
	if errors.Is(ownerErr, storeerr.ErrNotFound) {
		return access.HumanMachineAccess{}, storeerr.ErrNotFound
	}
	if ownerErr != nil {
		return access.HumanMachineAccess{}, ownerErr
	}
	if strings.TrimSpace(createdByUserID) != strings.TrimSpace(userID) {
		return access.HumanMachineAccess{}, storeerr.ErrForbidden
	}
	return access.HumanMachineAccess{
		WorkspaceMember: workspaceMember,
		Machine:         machine,
	}, nil
}

func (service *Service) RequireMachineManagementForUser(ctx context.Context, userID string, workspaceID string, machineID string) error {
	_, err := service.requireMachineManagement(ctx, userID, workspaceID, machineID)
	return err
}

func (service *Service) RotateMachineKeyForUser(ctx context.Context, userID string, workspaceID string, machineID string) (string, error) {
	if _, err := service.requireMachineManagement(ctx, userID, workspaceID, machineID); err != nil {
		return "", err
	}

	newAPIKey, err := secrets.NewToken(32)
	if err != nil {
		return "", fmt.Errorf("generate machine api key: %w", err)
	}
	commandTag, err := service.db.Exec(ctx, `
UPDATE machines
SET api_key = $3, updated_at = NOW()
WHERE workspace_id = $1 AND id = $2 AND status = 'active'
`, workspaceID, machineID, newAPIKey)
	if err != nil {
		return "", fmt.Errorf("rotate machine key: %w", err)
	}
	if commandTag.RowsAffected() == 0 {
		return "", storeerr.ErrNotFound
	}
	return newAPIKey, nil
}
