package server

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"os"
	"path/filepath"
	"sort"
	"strings"

	"gorm.io/gorm"

	"github.com/pingcap/agent-git-service/config"
	"github.com/pingcap/agent-git-service/internal/connectedlogin"
	"github.com/pingcap/agent-git-service/internal/crypto"
	"github.com/pingcap/agent-git-service/internal/db"
	"github.com/pingcap/agent-git-service/internal/embedding"
	"github.com/pingcap/agent-git-service/internal/gitstore"
	"github.com/pingcap/agent-git-service/internal/oidc"
	"github.com/pingcap/agent-git-service/internal/service"
	"github.com/pingcap/agent-git-service/internal/tenant"
	"github.com/pingcap/agent-git-service/internal/wikicatalog"
)

var errUnknownTenant = errors.New("unknown tenant")

type tenantRegistry struct {
	byID     map[tenant.ID]*service.TenantResources
	bindings []tenantDomainBinding
	ids      []tenant.ID
}

func initTenantRegistry(cfg config.Config) (*tenantRegistry, error) {
	tenantConfigs, err := canonicalizeTenantRoots(cfg.Tenants)
	if err != nil {
		return nil, err
	}
	registry := &tenantRegistry{
		byID: make(map[tenant.ID]*service.TenantResources, len(tenantConfigs)),
	}
	for _, tenantCfg := range tenantConfigs {
		if tenantCfg.Disabled {
			slog.Info("tenant disabled", "tenant_id", tenantCfg.ID)
			continue
		}
		resources, err := initTenantResources(cfg.Environment, tenantCfg)
		if err != nil {
			registry.close()
			return nil, fmt.Errorf("tenant %q: %w", tenantCfg.ID, err)
		}
		id := resources.Info.ID
		if _, exists := registry.byID[id]; exists {
			registry.close()
			return nil, fmt.Errorf("duplicate tenant %q", id)
		}
		registry.byID[id] = resources
		registry.ids = append(registry.ids, id)
		registry.bindings = append(registry.bindings, tenantDomainBinding{
			Info:    resources.Info,
			Aliases: append([]string(nil), tenantCfg.DomainAliases...),
		})
	}
	sort.Slice(registry.ids, func(i, j int) bool { return registry.ids[i] < registry.ids[j] })
	if len(registry.ids) == 0 {
		return nil, fmt.Errorf("at least one tenant is required")
	}
	return registry, nil
}

func canonicalizeTenantRoots(configs []config.TenantConfig) ([]config.TenantConfig, error) {
	prepared := append([]config.TenantConfig(nil), configs...)
	type rootOwner struct {
		tenantID string
		path     string
		info     os.FileInfo
	}
	roots := make([]rootOwner, 0, len(prepared))
	for index := range prepared {
		if prepared[index].Disabled {
			continue
		}
		root, err := gitstore.CanonicalizeRoot(prepared[index].GitRepoDir)
		if err != nil {
			return nil, fmt.Errorf("tenant %q Git root: %w", prepared[index].ID, err)
		}
		rootInfo, err := os.Stat(root)
		if err != nil {
			return nil, fmt.Errorf("tenant %q Git root identity: %w", prepared[index].ID, err)
		}
		for _, existing := range roots {
			if pathsOverlap(existing.path, root) {
				return nil, fmt.Errorf("tenant Git roots for %q and %q overlap after canonicalization", existing.tenantID, prepared[index].ID)
			}
			if os.SameFile(existing.info, rootInfo) {
				return nil, fmt.Errorf("tenant Git roots for %q and %q resolve to the same filesystem directory", existing.tenantID, prepared[index].ID)
			}
		}
		prepared[index].GitRepoDir = root
		roots = append(roots, rootOwner{tenantID: prepared[index].ID, path: root, info: rootInfo})
	}
	return prepared, nil
}

func pathsOverlap(left, right string) bool {
	if left == right {
		return true
	}
	leftToRight, err := filepath.Rel(left, right)
	if err == nil && leftToRight != ".." && !strings.HasPrefix(leftToRight, ".."+string(filepath.Separator)) {
		return true
	}
	rightToLeft, err := filepath.Rel(right, left)
	return err == nil && rightToLeft != ".." && !strings.HasPrefix(rightToLeft, ".."+string(filepath.Separator))
}

func initTenantResources(environment string, cfg config.TenantConfig) (*service.TenantResources, error) {
	id, err := tenant.ParseID(cfg.ID)
	if err != nil {
		return nil, err
	}
	if environment == "production" && cfg.AllowAnyToken {
		return nil, fmt.Errorf("ALLOW_ANY_TOKEN is not allowed in production")
	}
	info := tenant.Info{
		ID:                         id,
		CanonicalBaseURL:           cfg.BaseURL,
		ConsoleBaseURL:             cfg.Browser.ConsoleBaseURL,
		OAuthDeviceVerificationURL: cfg.Browser.OAuthDeviceVerificationURL,
		OAuthLoopbackRedirects:     cfg.Browser.OAuthLoopbackRedirects,
		CORSAllowedOrigins:         append([]string(nil), cfg.Browser.CORSAllowedOrigins...),
		ConnectedLoginEnabled:      cfg.ConnectedLogin.Enabled,
		OIDCEnabled:                cfg.OIDC.Enabled,
	}
	if err := info.Validate(); err != nil {
		return nil, err
	}

	database, err := db.Open(cfg.DBdsn)
	if err != nil {
		return nil, fmt.Errorf("database: %w", err)
	}
	closeDatabase := true
	defer func() {
		if closeDatabase {
			closeGormDB(database)
		}
	}()
	if err := db.ClaimTenant(database, id); err != nil {
		return nil, fmt.Errorf("database ownership: %w", err)
	}
	if err := db.MigrateWithTimeout(database); err != nil {
		return nil, fmt.Errorf("database migration: %w", err)
	}
	if err := db.EnforceTenantProvenance(database, info); err != nil {
		return nil, fmt.Errorf("tenant provenance: %w", err)
	}
	if environment == "development" {
		if err := db.Seed(database, cfg.AdminLogin, cfg.AdminToken); err != nil {
			return nil, fmt.Errorf("seed: %w", err)
		}
		slog.Info("tenant seed applied", "tenant_id", id, "environment", environment)
	}

	embedder := tenantEmbedder(cfg)
	if dims := embedder.Dimensions(); dims > 0 {
		db.InitVector(database, dims)
	}

	store, err := gitstore.NewForTenant(cfg.GitRepoDir, id)
	if err != nil {
		return nil, err
	}
	wikiBlob := wikicatalog.NewBlobStore(store.CanonicalRoot())
	wikiCatalog := wikicatalog.New(database, wikiBlob)

	secretBox, err := crypto.NewSecretBox(cfg.SecretEncryptionKey)
	if err != nil {
		return nil, fmt.Errorf("secret encryption: %w", err)
	}
	oidcProvider, err := tenantOIDCProvider(cfg)
	if err != nil {
		return nil, err
	}
	connectedProvider, err := tenantConnectedLoginProvider(cfg)
	if err != nil {
		return nil, err
	}

	resources := &service.TenantResources{
		Info:           info,
		DB:             database,
		Git:            store,
		WikiCatalog:    wikiCatalog,
		WikiBlob:       wikiBlob,
		SecretBox:      secretBox,
		Embedder:       embedder,
		AllowAnyToken:  cfg.AllowAnyToken,
		OIDC:           oidcProvider,
		ConnectedLogin: connectedProvider,
		Workflow: service.WorkflowPolicy{
			Enabled:   cfg.Workflow.Enabled,
			Image:     cfg.Workflow.Image,
			Timeout:   cfg.Workflow.Timeout,
			CPUs:      cfg.Workflow.CPUs,
			Memory:    cfg.Workflow.Memory,
			PidsLimit: cfg.Workflow.PidsLimit,
			NoFile:    cfg.Workflow.NoFile,
			TmpfsSize: cfg.Workflow.TmpfsSize,
		},
	}
	if err := resources.Validate(); err != nil {
		return nil, err
	}
	closeDatabase = false
	slog.Info("tenant resources initialized",
		"tenant_id", id,
		"canonical_host", info.Hostname(),
		"git_root", store.CanonicalRoot(),
		"embedding_enabled", !embedding.IsNop(embedder),
		"oidc_enabled", info.OIDCEnabled,
		"connected_login_enabled", info.ConnectedLoginEnabled,
	)
	return resources, nil
}

func tenantEmbedder(cfg config.TenantConfig) embedding.Embedder {
	if strings.TrimSpace(cfg.Embedding.APIKey) == "" {
		return embedding.NopEmbedder{}
	}
	opts := []embedding.OpenAIOption{
		embedding.WithBaseURL(cfg.Embedding.BaseURL),
		embedding.WithModel(cfg.Embedding.Model),
	}
	if cfg.Embedding.Dimensions > 0 {
		opts = append(opts, embedding.WithDimensions(cfg.Embedding.Dimensions))
	}
	return embedding.NewOpenAI(cfg.Embedding.APIKey, opts...)
}

func tenantOIDCProvider(cfg config.TenantConfig) (service.OIDCProvider, error) {
	if !cfg.OIDC.Enabled {
		return nil, nil
	}
	client, err := oidc.New(oidc.Config{
		Provider:          cfg.OIDC.Provider,
		Issuer:            cfg.OIDC.Issuer,
		DiscoveryURL:      cfg.OIDC.DiscoveryURL,
		ClientID:          cfg.OIDC.ClientID,
		ClientSecret:      cfg.OIDC.ClientSecret,
		Audience:          cfg.OIDC.Audience,
		Scopes:            cfg.OIDC.Scopes,
		AllowInsecureHTTP: cfg.OIDC.AllowInsecureHTTP,
	})
	if err != nil {
		return nil, fmt.Errorf("OIDC: %w", err)
	}
	return client, nil
}

func tenantConnectedLoginProvider(cfg config.TenantConfig) (service.ConnectedLoginProvider, error) {
	if !cfg.ConnectedLogin.Enabled {
		return nil, nil
	}
	client, err := connectedlogin.New(connectedlogin.Config{
		Provider:                  cfg.ConnectedLogin.Provider,
		Origin:                    cfg.ConnectedLogin.Origin,
		APIOrigin:                 cfg.ConnectedLogin.APIOrigin,
		ClientID:                  cfg.ConnectedLogin.ClientID,
		ClientSecret:              cfg.ConnectedLogin.ClientSecret,
		CallbackBaseURL:           cfg.BaseURL,
		LoginPath:                 cfg.ConnectedLogin.LoginPath,
		TokenPath:                 cfg.ConnectedLogin.TokenPath,
		UserinfoPath:              cfg.ConnectedLogin.UserinfoPath,
		ReturnToParam:             cfg.ConnectedLogin.ReturnToParam,
		SubjectClaim:              cfg.ConnectedLogin.SubjectClaim,
		SubjectNamespaceClaim:     cfg.ConnectedLogin.SubjectNamespaceClaim,
		SubjectNamespaceSlugClaim: cfg.ConnectedLogin.SubjectNamespaceSlugClaim,
		ActorTypeClaim:            cfg.ConnectedLogin.ActorTypeClaim,
		HumanTypeValue:            cfg.ConnectedLogin.HumanTypeValue,
		AgentTypeValue:            cfg.ConnectedLogin.AgentTypeValue,
		ClientIDClaim:             cfg.ConnectedLogin.ClientIDClaim,
		ClientNameClaim:           cfg.ConnectedLogin.ClientNameClaim,
		PreferredUsernameClaim:    cfg.ConnectedLogin.PreferredUsernameClaim,
		NameClaim:                 cfg.ConnectedLogin.NameClaim,
		PictureClaim:              cfg.ConnectedLogin.PictureClaim,
		AvatarURLClaim:            cfg.ConnectedLogin.AvatarURLClaim,
		DescriptionClaim:          cfg.ConnectedLogin.DescriptionClaim,
		ScopeClaim:                cfg.ConnectedLogin.ScopeClaim,
	})
	if err != nil {
		return nil, fmt.Errorf("connected login: %w", err)
	}
	return client, nil
}

func (r *tenantRegistry) ForTenant(id tenant.ID) (*service.TenantResources, error) {
	if r == nil {
		return nil, service.ErrTenantResourcesUnavailable
	}
	resources, ok := r.byID[id]
	if !ok {
		return nil, fmt.Errorf("%w %q", errUnknownTenant, id)
	}
	return resources, nil
}

func (r *tenantRegistry) tenantForContext(ctx context.Context) (*service.TenantResources, error) {
	info, err := tenant.Require(ctx)
	if err != nil {
		return nil, err
	}
	resources, err := r.ForTenant(info.ID)
	if err != nil {
		return nil, err
	}
	if !resources.Info.Equal(info) {
		return nil, fmt.Errorf("%w: context policy for %q does not match registry", service.ErrTenantResourceMismatch, info.ID)
	}
	return resources, nil
}

func (r *tenantRegistry) close() {
	if r == nil {
		return
	}
	for _, resources := range r.byID {
		closeGormDB(resources.DB)
	}
}

func (r *tenantRegistry) readinessDatabases() map[string]*gorm.DB {
	if r == nil {
		return map[string]*gorm.DB{}
	}
	out := make(map[string]*gorm.DB, len(r.byID))
	for id, resources := range r.byID {
		out["tenant_"+string(id)+"_db"] = resources.DB
	}
	return out
}

func (r *tenantRegistry) readinessStorage() map[string]tenantStorageReadiness {
	if r == nil {
		return map[string]tenantStorageReadiness{}
	}
	out := make(map[string]tenantStorageReadiness, len(r.byID))
	for id, resources := range r.byID {
		out[string(id)] = tenantStorageReadiness{
			TenantID: id,
			Git:      resources.Git,
			WikiBlob: resources.WikiBlob,
		}
	}
	return out
}

func closeGormDB(database *gorm.DB) {
	if database == nil {
		return
	}
	if sqlDB, err := database.DB(); err == nil {
		_ = sqlDB.Close()
	}
}
