package service

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"time"

	"gorm.io/gorm"

	"github.com/pingcap/agent-git-service/internal/crypto"
	"github.com/pingcap/agent-git-service/internal/embedding"
	"github.com/pingcap/agent-git-service/internal/gitstore"
	applog "github.com/pingcap/agent-git-service/internal/logging"
	"github.com/pingcap/agent-git-service/internal/tenant"
	"github.com/pingcap/agent-git-service/internal/wikicatalog"
)

var (
	ErrTenantResourcesUnavailable = errors.New("tenant resources unavailable")
	ErrTenantResourceMismatch     = errors.New("tenant resource identity mismatch")
	ErrTenantDBScopeMismatch      = errors.New("transaction database tenant mismatch")
)

// WorkflowPolicy is the tenant-owned workflow execution policy.
type WorkflowPolicy struct {
	Enabled   bool
	Image     string
	Timeout   time.Duration
	CPUs      string
	Memory    string
	PidsLimit int
	NoFile    int
	TmpfsSize string
}

// TenantResources owns every stateful dependency and policy for one tenant.
// The pointers are immutable after the registry has been published.
type TenantResources struct {
	Info tenant.Info

	DB          *gorm.DB
	Git         *gitstore.Store
	WikiCatalog *wikicatalog.Catalog
	WikiBlob    *wikicatalog.BlobStore
	SecretBox   *crypto.SecretBox
	Embedder    embedding.Embedder

	AllowAnyToken  bool
	OIDC           OIDCProvider
	ConnectedLogin ConnectedLoginProvider
	Workflow       WorkflowPolicy
}

// Validate checks the resource bundle before it can enter the registry.
func (r *TenantResources) Validate() error {
	if r == nil {
		return fmt.Errorf("%w: nil resource bundle", ErrTenantResourcesUnavailable)
	}
	if err := r.Info.Validate(); err != nil {
		return err
	}
	if r.DB == nil || r.Git == nil || r.WikiCatalog == nil || r.WikiBlob == nil || r.SecretBox == nil || r.Embedder == nil {
		return fmt.Errorf("%w for tenant %q: incomplete resource bundle", ErrTenantResourcesUnavailable, r.Info.ID)
	}
	if r.WikiCatalog.DB != r.DB || r.WikiCatalog.Blob != r.WikiBlob {
		return fmt.Errorf("%w for tenant %q: wiki catalog ownership does not match resource bundle", ErrTenantResourceMismatch, r.Info.ID)
	}
	if r.Info.OIDCEnabled != (r.OIDC != nil) {
		return fmt.Errorf("%w for tenant %q: OIDC policy and provider disagree", ErrTenantResourcesUnavailable, r.Info.ID)
	}
	if r.Info.ConnectedLoginEnabled != (r.ConnectedLogin != nil) {
		return fmt.Errorf("%w for tenant %q: connected-login policy and provider disagree", ErrTenantResourcesUnavailable, r.Info.ID)
	}
	return nil
}

// TenantResourcesProvider resolves initialized resources from a trusted
// immutable tenant ID.
type TenantResourcesProvider interface {
	ForTenant(tenant.ID) (*TenantResources, error)
}

// TenantScope is the resource view for one business operation. DB may be the
// tenant's root DB or a transaction explicitly bound to the same tenant.
type TenantScope struct {
	Info      tenant.Info
	Resources *TenantResources
	DB        *gorm.DB
}

// NewService constructs the one shared business service graph. Tenant-owned
// resources remain behind the required provider and are resolved from the
// validated context at business-operation boundaries.
func NewService(ctx context.Context, tenants TenantResourcesProvider) (*Service, error) {
	if tenants == nil {
		return nil, ErrTenantResourcesUnavailable
	}
	if ctx == nil {
		return nil, fmt.Errorf("%w: server context is required", ErrTenantResourcesUnavailable)
	}
	return &Service{
		serverCtx: ctx,
		tenants:   tenants,
	}, nil
}

// Scope resolves and verifies all tenant-owned dependencies for ctx.
func (s *Service) Scope(ctx context.Context) (TenantScope, error) {
	info, err := tenant.Require(ctx)
	if err != nil {
		return TenantScope{}, err
	}
	if s.tenants == nil {
		return TenantScope{}, ErrTenantResourcesUnavailable
	}
	resources, err := s.tenants.ForTenant(info.ID)
	if err != nil {
		return TenantScope{}, fmt.Errorf("%w for tenant %q: %v", ErrTenantResourcesUnavailable, info.ID, err)
	}
	if err := resources.Validate(); err != nil {
		return TenantScope{}, err
	}
	if !resources.Info.Equal(info) {
		return TenantScope{}, fmt.Errorf("%w: context policy for %q does not match registry", ErrTenantResourceMismatch, info.ID)
	}

	database := resources.DB
	if binding, ok := dbBindingFromContext(ctx); ok {
		if binding.TenantID == "" || binding.TenantID != info.ID || binding.owner != resources {
			return TenantScope{}, fmt.Errorf("%w: context=%q transaction=%q", ErrTenantDBScopeMismatch, info.ID, binding.TenantID)
		}
		if binding.DB == nil {
			return TenantScope{}, fmt.Errorf("%w: nil transaction database", ErrTenantDBScopeMismatch)
		}
		database = binding.DB
	}

	return TenantScope{
		Info:      info,
		Resources: resources,
		DB:        database.WithContext(ctx),
	}, nil
}

// RequireDB returns the tenant database or a same-tenant transaction.
func (s *Service) RequireDB(ctx context.Context) (*gorm.DB, error) {
	scope, err := s.Scope(ctx)
	if err != nil {
		return nil, err
	}
	return scope.DB, nil
}

// DBForTenantResources resolves a context-scoped database only when ctx owns
// the exact resource bundle expected by a tenant-bound component.
func (s *Service) DBForTenantResources(ctx context.Context, expected *TenantResources) *gorm.DB {
	scope, err := s.Scope(ctx)
	if err == nil && (expected == nil || scope.Resources != expected) {
		err = ErrTenantResourceMismatch
	}
	if err != nil {
		return s.failedDB(ctx, err)
	}
	return scope.DB
}

// contextForTenantEnvelope reconstructs trusted context for terminalizing
// detached work whose carried context is missing or mismatched. The envelope
// tenant ID is resolved through the registry; it is never treated as a raw
// resource selector or default.
func (s *Service) contextForTenantEnvelope(id tenant.ID) (context.Context, error) {
	if id == "" {
		return nil, ErrTenantResourcesUnavailable
	}
	resources, err := s.tenants.ForTenant(id)
	if err != nil {
		return nil, fmt.Errorf("%w for tenant %q: %v", ErrTenantResourcesUnavailable, id, err)
	}
	if err := resources.Validate(); err != nil {
		return nil, err
	}
	if resources.Info.ID != id {
		return nil, fmt.Errorf("%w: envelope=%q resources=%q", ErrTenantResourceMismatch, id, resources.Info.ID)
	}
	ctx := tenant.WithInfo(s.ServerCtx(), resources.Info)
	info, err := tenant.Require(ctx)
	if err != nil {
		return nil, err
	}
	if !info.Equal(resources.Info) {
		return nil, ErrTenantResourceMismatch
	}
	ctx = applog.WithRequestContext(ctx, slog.String("tenant_id", string(id)))
	return ctx, nil
}

func (s *Service) GitForCtx(ctx context.Context) (*gitstore.Store, error) {
	scope, err := s.Scope(ctx)
	if err != nil {
		return nil, err
	}
	return scope.Resources.Git, nil
}

func (s *Service) WikiCatalogForCtx(ctx context.Context) (*wikicatalog.Catalog, error) {
	scope, err := s.Scope(ctx)
	if err != nil {
		return nil, err
	}
	return scope.Resources.WikiCatalog, nil
}

func (s *Service) WikiBlobForCtx(ctx context.Context) (*wikicatalog.BlobStore, error) {
	scope, err := s.Scope(ctx)
	if err != nil {
		return nil, err
	}
	return scope.Resources.WikiBlob, nil
}

func (s *Service) SecretBoxForCtx(ctx context.Context) (*crypto.SecretBox, error) {
	scope, err := s.Scope(ctx)
	if err != nil {
		return nil, err
	}
	return scope.Resources.SecretBox, nil
}

func (s *Service) EmbedderForCtx(ctx context.Context) (embedding.Embedder, error) {
	scope, err := s.Scope(ctx)
	if err != nil {
		return nil, err
	}
	return scope.Resources.Embedder, nil
}

func (s *Service) OIDCForCtx(ctx context.Context) (OIDCProvider, error) {
	scope, err := s.Scope(ctx)
	if err != nil {
		return nil, err
	}
	if scope.Resources.OIDC == nil {
		return nil, ErrOIDCNotConfigured
	}
	return scope.Resources.OIDC, nil
}

func (s *Service) ConnectedLoginForCtx(ctx context.Context) (ConnectedLoginProvider, error) {
	scope, err := s.Scope(ctx)
	if err != nil {
		return nil, err
	}
	if scope.Resources.ConnectedLogin == nil {
		return nil, ErrConnectedLoginNotConfigured
	}
	return scope.Resources.ConnectedLogin, nil
}

func (s *Service) BaseURLFor(ctx context.Context) (string, error) {
	scope, err := s.Scope(ctx)
	if err != nil {
		return "", err
	}
	return scope.Info.BaseURL(), nil
}

func (s *Service) HTMLBaseURLFor(ctx context.Context) (string, error) {
	scope, err := s.Scope(ctx)
	if err != nil {
		return "", err
	}
	return scope.Info.HTMLBaseURL(), nil
}

func (s *Service) WorkflowPolicyForCtx(ctx context.Context) (WorkflowPolicy, error) {
	scope, err := s.Scope(ctx)
	if err != nil {
		return WorkflowPolicy{}, err
	}
	return scope.Resources.Workflow, nil
}

func (s *Service) AllowAnyTokenForCtx(ctx context.Context) (bool, error) {
	scope, err := s.Scope(ctx)
	if err != nil {
		return false, err
	}
	return scope.Resources.AllowAnyToken, nil
}
