package server

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

	"github.com/mem9-ai/drive9/pkg/meta"
	"github.com/mem9-ai/drive9/pkg/metrics"
	"github.com/mem9-ai/drive9/pkg/tenant"
)

// errTiDBCloudIAMNotEnabled is returned when the provisioner does not implement
// TiDB Cloud IAM identity resolution. Mapped to 404 by token control-plane handlers.
var errTiDBCloudIAMNotEnabled = errors.New("TiDB Cloud IAM identity lookup is not enabled")

// errTokensProviderUnsupported is returned when a control-plane token operation
// targets a tenant whose provider is not tidb_cloud_native / tidb_cloud_native_shared.
// Mapped to 409 Conflict.
var errTokensProviderUnsupported = errors.New("only tidb_cloud_native / tidb_cloud_native_shared are supported")

func (s *Server) resolveTiDBCloudIdentity(ctx context.Context, cred tenant.CredentialProvisionRequest, metricPath string) (*tenant.TiDBCloudAPIKeyIdentity, error) {
	if organizationID, ok := controlPlaneOrganizationID(ctx); ok {
		return &tenant.TiDBCloudAPIKeyIdentity{
			OrganizationID: organizationID,
		}, nil
	}
	if identity, ok := s.tidbCloudRBACCache.getIdentity(cred); ok {
		metrics.RecordTiDBCloudRBACCacheRequest(metricPath, "role", "hit")
		return &identity, nil
	}
	metrics.RecordTiDBCloudRBACCacheRequest(metricPath, "role", "miss")
	resolver, ok := s.provisioner.(tenant.TiDBCloudAPIKeyIdentityResolver)
	if !ok {
		return nil, errTiDBCloudIAMNotEnabled
	}
	identity, err := resolver.ResolveAPIKeyIdentity(ctx, cred)
	if err != nil {
		return nil, err
	}
	if identity == nil || strings.TrimSpace(identity.OrganizationID) == "" {
		return nil, fmt.Errorf("TiDB Cloud IAM identity is missing organization")
	}
	// Drive9's TiDB Cloud policy intentionally treats both owner roles as
	// organization-scoped operators. The IAM lookup replaces the old
	// cluster-list authorization path; project resource membership is not part
	// of the local tenant ownership model.
	if identity.Role != tenant.TiDBCloudRoleOrgOwner && identity.Role != tenant.TiDBCloudRoleProjectOwner {
		return nil, fmt.Errorf("%w: role %q; org:owner or project:owner is required", tenant.ErrTiDBCloudRoleInsufficient, identity.Role)
	}
	s.tidbCloudRBACCache.rememberIdentity(cred, *identity)
	return identity, nil
}

// Specific not-found / lookup variants for admin message fidelity. Control-plane
// handlers still map not-found variants to a uniform "tenant not found"
// (anti-enumeration). Lookup failures wrap these sentinels so callers use
// errors.Is instead of string matching.
var (
	errTenantOrgBindingNotFound   = errors.New("tenant tidbcloud organization binding not found")
	errTenantSharedDBNotFound     = errors.New("tenant shared DB pool not found")
	errTenantPoolMembershipLookup = errors.New("tenant pool membership lookup failed")
	errTenantSharedDBLookup       = errors.New("tenant shared DB pool lookup failed")
	errTenantOrgBindingLookup     = errors.New("tenant organization binding lookup failed")
)

// resolveTiDBCloudAuthorizedTenant resolves a tenant and verifies that
// organizationID owns it, forked by provider (native org binding vs shared DB
// pool). Free-pool tenants, deleted tenants, and missing tenants are folded
// into tenant.ErrQuotaBackendNotFound. Missing native binding returns
// errTenantOrgBindingNotFound; missing shared DB returns errTenantSharedDBNotFound.
// Unsupported providers return errTokensProviderUnsupported.
//
// On success the second return is the effective organization ID for the tenant
// (from the binding or shared pool). Callers that need finer status filtering
// (e.g. control-plane generate rejects suspended) inspect t.Status themselves.
func (s *Server) resolveTiDBCloudAuthorizedTenant(ctx context.Context, tenantID, organizationID string) (*meta.Tenant, string, error) {
	organizationID = strings.TrimSpace(organizationID)
	tenantID = strings.TrimSpace(tenantID)
	if tenantID == "" {
		return nil, "", tenant.ErrQuotaBackendNotFound
	}
	t, err := s.meta.GetTenant(ctx, tenantID)
	if err != nil {
		if errors.Is(err, meta.ErrNotFound) {
			return nil, "", tenant.ErrQuotaBackendNotFound
		}
		return nil, "", err
	}
	if t.Status == meta.TenantDeleted {
		return nil, "", tenant.ErrQuotaBackendNotFound
	}
	if t.Provider == tenant.ProviderTiDBCloudNativeShared {
		membership, membershipErr := s.meta.GetTenantPoolMembership(ctx, tenantID)
		if membershipErr == nil && membership.PoolStatus == meta.TenantPoolBindingFree {
			return nil, "", tenant.ErrQuotaBackendNotFound
		}
		if membershipErr != nil && !errors.Is(membershipErr, meta.ErrNotFound) {
			return nil, "", fmt.Errorf("%w: %v", errTenantPoolMembershipLookup, membershipErr)
		}
		physical, physicalErr := s.meta.GetSharedDBForTenant(ctx, t.ID)
		if physicalErr != nil {
			if errors.Is(physicalErr, meta.ErrNotFound) {
				return nil, "", errTenantSharedDBNotFound
			}
			return nil, "", fmt.Errorf("%w: %v", errTenantSharedDBLookup, physicalErr)
		}
		if physical == nil {
			return nil, "", errTenantSharedDBNotFound
		}
		if !tiDBCloudOrganizationMatches(organizationID, physical.TiDBCloudOrganizationID) {
			return nil, "", tenant.ErrQuotaPermissionDenied
		}
		return t, physical.TiDBCloudOrganizationID, nil
	}
	if t.Provider != tenant.ProviderTiDBCloudNative {
		return nil, "", errTokensProviderUnsupported
	}
	binding, err := s.meta.GetTenantTiDBCloudOrgBinding(ctx, tenantID)
	if err != nil {
		if errors.Is(err, meta.ErrNotFound) {
			return nil, "", errTenantOrgBindingNotFound
		}
		return nil, "", fmt.Errorf("%w: %v", errTenantOrgBindingLookup, err)
	}
	if binding.PoolStatus == meta.TenantPoolBindingFree {
		return nil, "", tenant.ErrQuotaBackendNotFound
	}
	if !tiDBCloudOrganizationMatches(organizationID, binding.OrganizationID) {
		return nil, "", tenant.ErrQuotaPermissionDenied
	}
	return t, binding.OrganizationID, nil
}

func tiDBCloudOrganizationMatches(identityOrganizationID, resourceOrganizationID string) bool {
	identityOrganizationID = strings.TrimSpace(identityOrganizationID)
	resourceOrganizationID = strings.TrimSpace(resourceOrganizationID)
	return identityOrganizationID != "" && resourceOrganizationID == identityOrganizationID
}

func (s *Server) authorizeNativeTenantCredentials(ctx context.Context, t *meta.Tenant, cred tenant.CredentialProvisionRequest, metricPath string) (*meta.TenantTiDBCloudOrgBinding, error) {
	if t == nil {
		return nil, tenant.ErrQuotaBackendNotFound
	}
	binding, err := s.meta.GetTenantTiDBCloudOrgBinding(ctx, t.ID)
	if err != nil {
		if errors.Is(err, meta.ErrNotFound) {
			return nil, tenant.ErrQuotaBackendNotFound
		}
		return nil, fmt.Errorf("get tenant TiDB Cloud organization binding: %w", err)
	}
	identity, err := s.resolveTiDBCloudIdentity(ctx, cred, metricPath)
	if err != nil {
		return nil, err
	}
	if !tiDBCloudOrganizationMatches(identity.OrganizationID, binding.OrganizationID) {
		return nil, tenant.ErrQuotaPermissionDenied
	}
	return binding, nil
}

func (s *Server) authorizeNativeTenantOrganization(ctx context.Context, t *meta.Tenant, organizationID string) (*meta.TenantTiDBCloudOrgBinding, error) {
	if t == nil {
		return nil, tenant.ErrQuotaBackendNotFound
	}
	binding, err := s.meta.GetTenantTiDBCloudOrgBinding(ctx, t.ID)
	if err != nil {
		if errors.Is(err, meta.ErrNotFound) {
			return nil, tenant.ErrQuotaBackendNotFound
		}
		return nil, fmt.Errorf("get tenant TiDB Cloud organization binding: %w", err)
	}
	if !tiDBCloudOrganizationMatches(organizationID, binding.OrganizationID) {
		return nil, tenant.ErrQuotaPermissionDenied
	}
	return binding, nil
}

func isTiDBCloudRoleInsufficient(err error) bool {
	return errors.Is(err, tenant.ErrTiDBCloudRoleInsufficient)
}
