package providertokens

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"net/url"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/core/secrets"
	store "github.com/tidbcloud/AgentTeam/internal/store/providertokens"
	"gorm.io/gorm"
)

// Options configures the personal credential service clock, encryption key,
// and linked-source port.
type Options struct {
	Now            func() time.Time
	CredentialsKey []byte
	LinkCounter    LinkedSourceCounter
}

// Service owns personal external credential encryption, owner authorization,
// lifecycle, and metadata projection over the dedicated persistence store.
type Service struct {
	store          *store.Store
	now            func() time.Time
	credentialsKey []byte
	linkCounter    LinkedSourceCounter
}

// New constructs the owner-authorized personal credential service. Callers
// must provide the server-side encryption key before creating or rotating
// credentials; plaintext is never written to the store.
func New(s *store.Store, options Options) *Service {
	now := options.Now
	if now == nil {
		now = func() time.Time { return time.Now().UTC() }
	}
	return &Service{
		store:          s,
		now:            now,
		credentialsKey: options.CredentialsKey,
		linkCounter:    options.LinkCounter,
	}
}

// List returns only metadata for credentials owned by the authenticated
// principal in the requested workspace. It never decrypts or returns secrets.
func (s *Service) List(ctx context.Context, request ListRequest) (ListResponse, error) {
	if err := validateContext(request.WorkspaceID, request.ActorPrincipalID); err != nil {
		return ListResponse{}, err
	}
	providerBaseURL := ""
	if strings.TrimSpace(request.ProviderBaseURL) != "" {
		normalized, err := NormalizeProviderBaseURL(request.ProviderBaseURL)
		if err != nil {
			return ListResponse{}, err
		}
		providerBaseURL = normalized
	}
	status := normalizePersistedStatusFilter(request.Status)
	responseStatus := normalizeResponseStatusFilter(request.Status)
	now := s.now()
	expiresAfter, expiresAtOrBefore := expiryFiltersForResponseStatus(responseStatus, now)
	tokens, err := s.store.ListOwned(ctx, store.ListOptions{
		WorkspaceID:       request.WorkspaceID,
		OwnerPrincipalID:  request.ActorPrincipalID,
		ProviderBaseURL:   providerBaseURL,
		Status:            status,
		ExpiresAfter:      expiresAfter,
		ExpiresAtOrBefore: expiresAtOrBefore,
		Limit:             request.Limit,
		IncludeCiphertext: false,
	})
	if err != nil {
		return ListResponse{}, storeError(err)
	}
	counts, err := s.linkedSourceCounts(ctx, request.WorkspaceID, tokens)
	if err != nil {
		return ListResponse{}, err
	}
	items := make([]TokenRef, 0, len(tokens))
	usage := strings.TrimSpace(request.Usage)
	for _, token := range tokens {
		ref := toRef(token, now, counts[CredentialRef(token.ID)])
		if responseStatus != "" && ref.Status != responseStatus {
			continue
		}
		if usage != "" && !ContainsUsage(ref.AllowedUsages, usage) {
			continue
		}
		items = append(items, ref)
	}
	return ListResponse{Items: items}, nil
}

// Create validates and encrypts a user-supplied provider credential before
// persistence, returning only its non-secret service projection.
func (s *Service) Create(ctx context.Context, request CreateRequest) (TokenRef, error) {
	if err := validateContext(request.WorkspaceID, request.ActorPrincipalID); err != nil {
		return TokenRef{}, err
	}
	displayName := strings.TrimSpace(request.DisplayName)
	if displayName == "" {
		return TokenRef{}, E(ErrInvalidRequest, "displayName is required")
	}
	if len(displayName) > 255 {
		return TokenRef{}, E(ErrInvalidRequest, "displayName must be 255 characters or fewer")
	}
	plaintext := strings.TrimSpace(request.AccessToken)
	if plaintext == "" {
		return TokenRef{}, E(ErrInvalidRequest, "accessToken is required")
	}
	providerType, err := normalizeProviderType(request.ProviderType)
	if err != nil {
		return TokenRef{}, err
	}
	providerBaseURL, err := NormalizeProviderBaseURL(request.ProviderBaseURL)
	if err != nil {
		return TokenRef{}, err
	}
	usages, err := normalizeUsages(request.AllowedUsages)
	if err != nil {
		return TokenRef{}, err
	}
	username := strings.TrimSpace(request.Username)
	if username == "" {
		username = "x-access-token"
	}
	if len(username) > 255 {
		return TokenRef{}, E(ErrInvalidRequest, "username must be 255 characters or fewer")
	}
	ciphertext, err := s.seal(plaintext)
	if err != nil {
		return TokenRef{}, err
	}
	usagesJSON, err := marshalUsages(usages)
	if err != nil {
		return TokenRef{}, err
	}
	now := s.now()
	id, err := randomID()
	if err != nil {
		return TokenRef{}, err
	}
	token, err := s.store.Create(ctx, store.PersonalAccessToken{
		ID:                id,
		WorkspaceID:       strings.TrimSpace(request.WorkspaceID),
		OwnerPrincipalID:  strings.TrimSpace(request.ActorPrincipalID),
		DisplayName:       displayName,
		ProviderType:      providerType,
		ProviderBaseURL:   providerBaseURL,
		Username:          username,
		AllowedUsagesJSON: usagesJSON,
		ValueCiphertext:   ciphertext,
		Status:            StatusActive,
		ExpiresAt:         request.ExpiresAt,
		CreatedBy:         strings.TrimSpace(request.ActorPrincipalID),
		UpdatedBy:         strings.TrimSpace(request.ActorPrincipalID),
		CreatedAt:         now,
		UpdatedAt:         now,
	})
	if err != nil {
		return TokenRef{}, storeError(err)
	}
	return toRef(token, now, 0), nil
}

// Get returns non-secret metadata only when the credential belongs to the
// authenticated principal in the requested workspace.
func (s *Service) Get(ctx context.Context, request GetRequest) (TokenRef, error) {
	token, err := s.getOwned(ctx, request.WorkspaceID, request.ActorPrincipalID, request.ID)
	if err != nil {
		return TokenRef{}, err
	}
	count, err := s.linkedSourceCount(ctx, request.WorkspaceID, CredentialRef(token.ID))
	if err != nil {
		return TokenRef{}, err
	}
	return toRef(token, s.now(), count), nil
}

// Update changes an owner-visible credential and encrypts replacement
// plaintext before persistence. Omitted fields retain their current values.
func (s *Service) Update(ctx context.Context, request UpdateRequest) (TokenRef, error) {
	token, err := s.getOwned(ctx, request.WorkspaceID, request.ActorPrincipalID, request.ID)
	if err != nil {
		return TokenRef{}, err
	}
	if request.DisplayName != nil {
		displayName := strings.TrimSpace(*request.DisplayName)
		if displayName == "" {
			return TokenRef{}, E(ErrInvalidRequest, "displayName is required")
		}
		if len(displayName) > 255 {
			return TokenRef{}, E(ErrInvalidRequest, "displayName must be 255 characters or fewer")
		}
		token.DisplayName = displayName
	}
	if request.ProviderType != nil {
		providerType, err := normalizeProviderType(*request.ProviderType)
		if err != nil {
			return TokenRef{}, err
		}
		token.ProviderType = providerType
	}
	if request.ProviderBaseURL != nil {
		providerBaseURL, err := NormalizeProviderBaseURL(*request.ProviderBaseURL)
		if err != nil {
			return TokenRef{}, err
		}
		token.ProviderBaseURL = providerBaseURL
	}
	if request.Username != nil {
		username := strings.TrimSpace(*request.Username)
		if username == "" {
			username = "x-access-token"
		}
		if len(username) > 255 {
			return TokenRef{}, E(ErrInvalidRequest, "username must be 255 characters or fewer")
		}
		token.Username = username
	}
	if request.AllowedUsages != nil {
		usages, err := normalizeUsages(request.AllowedUsages)
		if err != nil {
			return TokenRef{}, err
		}
		usagesJSON, err := marshalUsages(usages)
		if err != nil {
			return TokenRef{}, err
		}
		token.AllowedUsagesJSON = usagesJSON
	}
	if request.AccessToken != nil {
		plaintext := strings.TrimSpace(*request.AccessToken)
		if plaintext == "" {
			return TokenRef{}, E(ErrInvalidRequest, "accessToken is required when rotating a token")
		}
		ciphertext, err := s.seal(plaintext)
		if err != nil {
			return TokenRef{}, err
		}
		token.ValueCiphertext = ciphertext
	}
	if request.ClearExpiresAt {
		token.ExpiresAt = nil
	} else if request.ExpiresAt != nil {
		token.ExpiresAt = request.ExpiresAt
	}
	token.UpdatedBy = strings.TrimSpace(request.ActorPrincipalID)
	token.UpdatedAt = s.now()
	updated, err := s.store.Update(ctx, token)
	if err != nil {
		return TokenRef{}, storeError(err)
	}
	count, err := s.linkedSourceCount(ctx, request.WorkspaceID, CredentialRef(updated.ID))
	if err != nil {
		return TokenRef{}, err
	}
	return toRef(updated, s.now(), count), nil
}

// Revoke disables an owner-visible credential unless a KB source still
// references it. Revoking an already revoked credential is idempotent.
func (s *Service) Revoke(ctx context.Context, request RevokeRequest) (TokenRef, error) {
	token, err := s.getOwned(ctx, request.WorkspaceID, request.ActorPrincipalID, request.ID)
	if err != nil {
		return TokenRef{}, err
	}
	credentialRef := CredentialRef(token.ID)
	count, err := s.linkedSourceCount(ctx, request.WorkspaceID, credentialRef)
	if err != nil {
		return TokenRef{}, err
	}
	if count > 0 {
		return TokenRef{}, E(ErrTokenStateConflict, "personal access token is still linked by knowledge base sources")
	}
	if token.Status == StatusRevoked {
		return toRef(token, s.now(), 0), nil
	}
	now := s.now()
	token.Status = StatusRevoked
	token.RevokedAt = &now
	token.UpdatedBy = strings.TrimSpace(request.ActorPrincipalID)
	token.UpdatedAt = now
	updated, err := s.store.Update(ctx, token)
	if err != nil {
		return TokenRef{}, storeError(err)
	}
	return toRef(updated, now, 0), nil
}

// Delete permanently removes an owner-visible credential only after its KB
// source reference count reaches zero.
func (s *Service) Delete(ctx context.Context, request DeleteRequest) error {
	token, err := s.getOwned(ctx, request.WorkspaceID, request.ActorPrincipalID, request.ID)
	if err != nil {
		return err
	}
	count, err := s.linkedSourceCount(ctx, request.WorkspaceID, CredentialRef(token.ID))
	if err != nil {
		return err
	}
	if count > 0 {
		return E(ErrTokenStateConflict, "personal access token is still linked by knowledge base sources")
	}
	if err := s.store.Delete(ctx, request.WorkspaceID, token.ID); err != nil {
		return storeError(err)
	}
	return nil
}

// CredentialRef builds the opaque service reference used by linked resource
// domains; it does not expose or encode credential plaintext.
func CredentialRef(id string) string {
	return CredentialRefPrefix + strings.TrimSpace(id)
}

// IDFromCredentialRef parses a personal credential reference and rejects
// empty IDs or references owned by another credential scheme.
func IDFromCredentialRef(ref string) (string, bool) {
	trimmed := strings.TrimSpace(ref)
	if !strings.HasPrefix(trimmed, CredentialRefPrefix) {
		return "", false
	}
	id := strings.TrimSpace(strings.TrimPrefix(trimmed, CredentialRefPrefix))
	return id, id != ""
}

// NormalizeProviderBaseURL canonicalizes an HTTP(S) provider origin or host
// for matching and persistence; it does not authorize a remote repository.
func NormalizeProviderBaseURL(raw string) (string, error) {
	trimmed := strings.TrimSpace(raw)
	if trimmed == "" {
		return "", E(ErrInvalidRequest, "providerBaseUrl is required")
	}
	parsed, err := url.Parse(trimmed)
	if err != nil || parsed.Host == "" {
		parsed, err = url.Parse("https://" + strings.TrimPrefix(trimmed, "//"))
	}
	if err != nil || parsed.Host == "" {
		return "", E(ErrInvalidRequest, "providerBaseUrl must be an absolute http(s) URL or host")
	}
	scheme := strings.ToLower(strings.TrimSpace(parsed.Scheme))
	if scheme == "" {
		scheme = "https"
	}
	if scheme != "http" && scheme != "https" {
		return "", E(ErrInvalidRequest, "providerBaseUrl must use http or https")
	}
	normalized := url.URL{
		Scheme: scheme,
		Host:   strings.ToLower(parsed.Host),
		Path:   strings.TrimRight(parsed.EscapedPath(), "/"),
	}
	if normalized.Path == "/" {
		normalized.Path = ""
	}
	return normalized.String(), nil
}

// ProviderBaseURLFromRemoteURL derives the canonical provider origin from an
// HTTP(S) or scp-like Git remote without resolving or contacting the provider.
func ProviderBaseURLFromRemoteURL(raw string) (string, error) {
	trimmed := strings.TrimSpace(raw)
	if trimmed == "" {
		return "", E(ErrInvalidRequest, "remoteUrl is required")
	}
	parsed, err := url.Parse(trimmed)
	if err == nil && parsed.Host != "" && (parsed.Scheme == "http" || parsed.Scheme == "https") {
		return NormalizeProviderBaseURL(parsed.Scheme + "://" + parsed.Host)
	}
	scpLike := strings.SplitN(trimmed, ":", 2)
	if len(scpLike) == 2 {
		host := scpLike[0]
		if at := strings.LastIndex(host, "@"); at >= 0 {
			host = host[at+1:]
		}
		if strings.TrimSpace(host) != "" {
			return NormalizeProviderBaseURL(host)
		}
	}
	return "", E(ErrInvalidRequest, "remoteUrl must be an http(s) or scp-like git URL")
}

// RemoteURLMatchesProviderBaseURL reports whether a Git remote lies under the
// normalized provider origin and optional path prefix. It is a routing check,
// not proof that the credential grants repository access.
func RemoteURLMatchesProviderBaseURL(remoteURL, providerBaseURL string) (bool, error) {
	normalizedProviderBaseURL, err := NormalizeProviderBaseURL(providerBaseURL)
	if err != nil {
		return false, err
	}
	provider, err := url.Parse(normalizedProviderBaseURL)
	if err != nil || provider.Host == "" {
		return false, E(ErrInvalidRequest, "providerBaseUrl must be an absolute http(s) URL or host")
	}
	trimmedRemote := strings.TrimSpace(remoteURL)
	if trimmedRemote == "" {
		return false, E(ErrInvalidRequest, "remoteUrl is required")
	}
	if parsed, err := url.Parse(trimmedRemote); err == nil && parsed.Host != "" && (parsed.Scheme == "http" || parsed.Scheme == "https") {
		return httpRemoteMatchesProviderBaseURL(parsed, provider), nil
	}
	host, remotePath, ok := parseSCPLikeRemote(trimmedRemote)
	if !ok {
		return false, E(ErrInvalidRequest, "remoteUrl must be an http(s) or scp-like git URL")
	}
	if provider.Scheme != "https" || !strings.EqualFold(strings.TrimSpace(host), provider.Host) {
		return false, nil
	}
	return providerPathMatchesRemotePath(provider.EscapedPath(), remotePath), nil
}

func httpRemoteMatchesProviderBaseURL(remote *url.URL, provider *url.URL) bool {
	if remote == nil || provider == nil {
		return false
	}
	if !strings.EqualFold(strings.TrimSpace(remote.Scheme), strings.TrimSpace(provider.Scheme)) {
		return false
	}
	if !strings.EqualFold(strings.TrimSpace(remote.Host), strings.TrimSpace(provider.Host)) {
		return false
	}
	return providerPathMatchesRemotePath(provider.EscapedPath(), remote.EscapedPath())
}

func providerPathMatchesRemotePath(providerPath, remotePath string) bool {
	providerPath = strings.TrimRight("/"+strings.Trim(providerPath, "/"), "/")
	if providerPath == "" {
		return true
	}
	remotePath = strings.TrimRight("/"+strings.Trim(remotePath, "/"), "/")
	if remotePath == "" {
		remotePath = "/"
	}
	return remotePath == providerPath || strings.HasPrefix(remotePath, providerPath+"/")
}

func parseSCPLikeRemote(remoteURL string) (string, string, bool) {
	scpLike := strings.SplitN(strings.TrimSpace(remoteURL), ":", 2)
	if len(scpLike) != 2 {
		return "", "", false
	}
	host := strings.TrimSpace(scpLike[0])
	if at := strings.LastIndex(host, "@"); at >= 0 {
		host = host[at+1:]
	}
	path := strings.TrimSpace(scpLike[1])
	return host, path, host != "" && path != ""
}

func (s *Service) getOwned(ctx context.Context, workspaceID, actorPrincipalID, id string) (store.PersonalAccessToken, error) {
	if err := validateGetLike(workspaceID, actorPrincipalID, id); err != nil {
		return store.PersonalAccessToken{}, err
	}
	token, err := s.store.GetOwned(ctx, workspaceID, actorPrincipalID, id)
	if err != nil {
		return store.PersonalAccessToken{}, storeError(err)
	}
	return token, nil
}

func validateContext(workspaceID, actorPrincipalID string) error {
	if strings.TrimSpace(workspaceID) == "" {
		return E(ErrPermissionDenied, "workspace context is required")
	}
	if strings.TrimSpace(actorPrincipalID) == "" {
		return E(ErrPermissionDenied, "authenticated principal is required")
	}
	return nil
}

func validateGetLike(workspaceID, actorPrincipalID, id string) error {
	if err := validateContext(workspaceID, actorPrincipalID); err != nil {
		return err
	}
	if strings.TrimSpace(id) == "" {
		return E(ErrInvalidRequest, "token id is required")
	}
	return nil
}

func normalizeProviderType(raw string) (string, error) {
	switch strings.ToLower(strings.TrimSpace(raw)) {
	case "", ProviderGenericGit:
		return ProviderGenericGit, nil
	case ProviderGitHub:
		return ProviderGitHub, nil
	case ProviderGitea:
		return ProviderGitea, nil
	case ProviderGitLab:
		return ProviderGitLab, nil
	default:
		return "", E(ErrInvalidRequest, "providerType is not supported")
	}
}

func normalizeUsages(raw []string) ([]string, error) {
	if len(raw) == 0 {
		return []string{UsageKBMirror}, nil
	}
	out := make([]string, 0, len(raw))
	seen := make(map[string]struct{}, len(raw))
	for _, usage := range raw {
		usage = strings.ToLower(strings.TrimSpace(usage))
		if usage == "" {
			continue
		}
		if usage != UsageKBMirror {
			return nil, E(ErrInvalidRequest, "allowedUsages contains an unsupported usage")
		}
		if _, ok := seen[usage]; ok {
			continue
		}
		seen[usage] = struct{}{}
		out = append(out, usage)
	}
	if len(out) == 0 {
		return nil, E(ErrInvalidRequest, "allowedUsages is required")
	}
	return out, nil
}

func marshalUsages(usages []string) (string, error) {
	data, err := json.Marshal(usages)
	if err != nil {
		return "", fmt.Errorf("marshal personal access token usages: %w", err)
	}
	return string(data), nil
}

// DecodeAllowedUsages decodes supported service usage values from persistence;
// malformed or unknown values are ignored rather than promoted to authority.
func DecodeAllowedUsages(raw string) []string {
	var usages []string
	if err := json.Unmarshal([]byte(strings.TrimSpace(raw)), &usages); err != nil {
		return nil
	}
	out := make([]string, 0, len(usages))
	seen := make(map[string]struct{}, len(usages))
	for _, usage := range usages {
		usage = strings.ToLower(strings.TrimSpace(usage))
		if usage != UsageKBMirror {
			continue
		}
		if _, ok := seen[usage]; ok {
			continue
		}
		seen[usage] = struct{}{}
		out = append(out, usage)
	}
	return out
}

// ContainsUsage reports whether normalized service metadata permits one known
// usage; an empty requested usage means no additional filter.
func ContainsUsage(usages []string, want string) bool {
	want = strings.ToLower(strings.TrimSpace(want))
	if want == "" {
		return true
	}
	for _, usage := range usages {
		if strings.TrimSpace(usage) == want {
			return true
		}
	}
	return false
}

func normalizePersistedStatusFilter(status string) string {
	switch strings.ToLower(strings.TrimSpace(status)) {
	case StatusActive, StatusExpired:
		return StatusActive
	case StatusRevoked:
		return StatusRevoked
	default:
		return ""
	}
}

func expiryFiltersForResponseStatus(status string, now time.Time) (*time.Time, *time.Time) {
	switch status {
	case StatusActive:
		return &now, nil
	case StatusExpired:
		return nil, &now
	default:
		return nil, nil
	}
}

func normalizeResponseStatusFilter(status string) string {
	switch strings.ToLower(strings.TrimSpace(status)) {
	case StatusActive:
		return StatusActive
	case StatusRevoked:
		return StatusRevoked
	case StatusExpired:
		return StatusExpired
	default:
		return ""
	}
}

func (s *Service) seal(plaintext string) (string, error) {
	c, err := secrets.NewAESGCMCodec(s.credentialsKey)
	if err != nil {
		return "", E(ErrStorageUnavailable, "personal access token credential storage is not configured on server")
	}
	ciphertext, err := c.Seal(plaintext)
	if err != nil {
		return "", fmt.Errorf("seal personal access token credential: %w", err)
	}
	return ciphertext, nil
}

func (s *Service) linkedSourceCounts(ctx context.Context, workspaceID string, tokens []store.PersonalAccessToken) (map[string]int64, error) {
	refs := make([]string, 0, len(tokens))
	for _, token := range tokens {
		refs = append(refs, CredentialRef(token.ID))
	}
	if len(refs) == 0 || s.linkCounter == nil {
		return map[string]int64{}, nil
	}
	counts, err := s.linkCounter.CountSourceMirrorsByCredentialRefs(ctx, workspaceID, refs)
	if err != nil {
		return nil, fmt.Errorf("count linked knowledge base sources: %w", err)
	}
	return counts, nil
}

func (s *Service) linkedSourceCount(ctx context.Context, workspaceID, credentialRef string) (int64, error) {
	counts, err := s.linkedSourceCounts(ctx, workspaceID, []store.PersonalAccessToken{{ID: strings.TrimPrefix(credentialRef, CredentialRefPrefix)}})
	if err != nil {
		return 0, err
	}
	return counts[credentialRef], nil
}

func toRef(token store.PersonalAccessToken, now time.Time, linkedSourceCount int64) TokenRef {
	status := token.Status
	if status == StatusActive && token.ExpiresAt != nil && !token.ExpiresAt.After(now) {
		status = StatusExpired
	}
	return TokenRef{
		ID:                token.ID,
		CredentialRef:     CredentialRef(token.ID),
		WorkspaceID:       token.WorkspaceID,
		OwnerPrincipalID:  token.OwnerPrincipalID,
		DisplayName:       token.DisplayName,
		ProviderType:      token.ProviderType,
		ProviderBaseURL:   token.ProviderBaseURL,
		Username:          token.Username,
		AllowedUsages:     DecodeAllowedUsages(token.AllowedUsagesJSON),
		Status:            status,
		LinkedSourceCount: linkedSourceCount,
		LastUsedAt:        token.LastUsedAt,
		ExpiresAt:         token.ExpiresAt,
		RevokedAt:         token.RevokedAt,
		CreatedAt:         token.CreatedAt,
		UpdatedAt:         token.UpdatedAt,
	}
}

func storeError(err error) error {
	if err == nil {
		return nil
	}
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return E(ErrTokenNotFound, "personal access token not found")
	}
	return err
}

func randomID() (string, error) {
	var bytes [16]byte
	if _, err := rand.Read(bytes[:]); err != nil {
		return "", fmt.Errorf("generate personal access token id: %w", err)
	}
	return "pat_" + hex.EncodeToString(bytes[:]), nil
}
