package authentik

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

	"github.com/jackc/pgx/v5/pgxpool"

	"github.com/ultisuite/ulti-backend/internal/config"
)

const (
	provisionAttempts = 30
	provisionDelay    = 5 * time.Second
)

// StartProvisioner runs Authentik suite app provisioning in the background until success or ctx cancel.
func StartProvisioner(ctx context.Context, pool *pgxpool.Pool, cfg *config.Config) {
	if cfg == nil || cfg.AuthentikAPIToken == "" {
		slog.Info("authentik api provisioning skipped (AUTHENTIK_API_TOKEN not set; using blueprints)")
		return
	}
	go func() {
		for attempt := 1; attempt <= provisionAttempts; attempt++ {
			if ctx.Err() != nil {
				return
			}
			if err := EnsureSuiteApplications(ctx, pool, cfg); err != nil {
				slog.Warn("authentik suite provisioning failed, retrying",
					"attempt", attempt,
					"max", provisionAttempts,
					"error", err,
				)
				select {
				case <-ctx.Done():
					return
				case <-time.After(provisionDelay):
				}
				continue
			}
			if attempt > 1 {
				slog.Info("authentik suite provisioning ready", "attempt", attempt)
			}
			return
		}
		slog.Error("authentik suite provisioning gave up after retries", "attempts", provisionAttempts)
	}()
}

// EnsureSuiteApplications creates or adopts Authentik OAuth apps for enabled suite integrations.
func EnsureSuiteApplications(ctx context.Context, pool *pgxpool.Pool, cfg *config.Config) error {
	if cfg.AuthentikAPIToken == "" {
		return nil
	}
	client := NewClient(cfg.AuthentikAPIURL, cfg.AuthentikAPIToken)
	if err := client.Ping(ctx); err != nil {
		return err
	}

	authFlow, err := client.FindFlowBySlug(ctx, "default-provider-authorization-implicit-consent")
	if err != nil {
		return err
	}
	invalidFlow, err := client.FindFlowBySlug(ctx, "default-provider-invalidation-flow")
	if err != nil {
		return err
	}
	signingKey, err := client.FindSigningKeyPK(ctx)
	if err != nil {
		return err
	}

	scopePKs, err := loadStandardScopeMappings(ctx, client)
	if err != nil {
		return err
	}

	for _, spec := range Catalog(cfg) {
		if spec.Enabled != nil && !spec.Enabled(cfg) {
			continue
		}
		clientID, clientSecret, ok := resolveCredentials(spec, cfg)
		if !ok {
			slog.Warn("authentik app skipped: missing client credentials", "app", spec.Key)
			continue
		}
		redirects := spec.RedirectURIs(cfg)
		launchURL := ""
		if spec.LaunchURL != nil {
			launchURL = spec.LaunchURL(cfg)
		}

		if provisioned, err := IsProvisioned(ctx, pool, spec.Key); err != nil {
			return err
		} else if provisioned {
			if err := syncRedirects(ctx, pool, client, spec.Key, redirects); err != nil {
				slog.Warn("authentik redirect sync failed", "app", spec.Key, "error", err)
			}
			continue
		}

		providerPK, appPK, err := ensureApp(ctx, client, spec, clientID, clientSecret, launchURL, redirects, authFlow, invalidFlow, signingKey, scopePKs)
		if err != nil {
			slog.Warn("authentik app provision failed", "app", spec.Key, "error", err)
			continue
		}

		now := time.Now()
		if err := SaveProvisioned(ctx, pool, ProvisionRecord{
			AppKey:        spec.Key,
			AuthentikSlug: spec.Slug,
			ClientID:      clientID,
			ProviderID:    &providerPK,
			ApplicationID: &appPK,
			ProvisionedAt: now,
		}); err != nil {
			return err
		}
		slog.Info("authentik app provisioned", "app", spec.Key, "slug", spec.Slug, "client_id", clientID)
	}
	return nil
}

func syncRedirects(ctx context.Context, pool *pgxpool.Pool, client *Client, appKey string, redirects []string) error {
	rec, err := GetProvisioned(ctx, pool, appKey)
	if err != nil {
		return err
	}
	if rec.ProviderID == nil || *rec.ProviderID == 0 {
		return nil
	}
	return client.UpdateOAuth2ProviderRedirects(ctx, *rec.ProviderID, redirects)
}

func ensureApp(
	ctx context.Context,
	client *Client,
	spec AppSpec,
	clientID, clientSecret, launchURL string,
	redirects []string,
	authFlow, invalidFlow, signingKey string,
	scopePKs []string,
) (providerPK, appPK int, err error) {
	if app, found, err := client.FindApplicationBySlug(ctx, spec.Slug); err != nil {
		return 0, 0, err
	} else if found {
		providerPK = app.Provider
		if providerPK == 0 {
			if prov, ok, err := client.FindOAuth2ProviderByName(ctx, spec.ProviderName); err != nil {
				return 0, 0, err
			} else if ok {
				providerPK = prov.PK
			}
		}
		if providerPK != 0 {
			_ = client.UpdateOAuth2ProviderRedirects(ctx, providerPK, redirects)
		}
		return providerPK, app.PK, nil
	}

	if prov, found, err := client.FindOAuth2ProviderByName(ctx, spec.ProviderName); err != nil {
		return 0, 0, err
	} else if found {
		providerPK = prov.PK
		_ = client.UpdateOAuth2ProviderRedirects(ctx, providerPK, redirects)
	} else {
		mappings := scopePKs
		providerPK, err = client.CreateOAuth2Provider(ctx, CreateOAuth2ProviderRequest{
			Name:                spec.ProviderName,
			ClientID:            clientID,
			ClientSecret:        clientSecret,
			RedirectURIs:        redirects,
			AuthorizationFlowPK: authFlow,
			InvalidationFlowPK:  invalidFlow,
			SigningKeyPK:        signingKey,
			PropertyMappingPKs:  mappings,
			OfflineAccess:       spec.OfflineAccess,
		})
		if err != nil {
			return 0, 0, err
		}
	}

	appPK, err = client.CreateApplication(ctx, CreateApplicationRequest{
		Name:      spec.DisplayName,
		Slug:      spec.Slug,
		Group:     suiteGroup,
		LaunchURL: launchURL,
		Provider:  providerPK,
	})
	if err != nil {
		return providerPK, 0, err
	}
	return providerPK, appPK, nil
}

func loadStandardScopeMappings(ctx context.Context, client *Client) ([]string, error) {
	var pks []string
	for _, name := range []string{"openid", "email", "profile"} {
		pk, err := client.FindScopeMappingPK(ctx, name)
		if err != nil {
			return nil, err
		}
		pks = append(pks, pk)
	}
	return pks, nil
}

func resolveCredentials(spec AppSpec, cfg *config.Config) (clientID, clientSecret string, ok bool) {
	if spec.ClientID != nil {
		clientID = spec.ClientID(cfg)
	}
	if spec.ClientSecret != nil {
		clientSecret = spec.ClientSecret(cfg)
	}
	if clientID == "" {
		clientID = defaultClientID(spec.Key)
	}
	if clientSecret == "" {
		clientSecret = defaultClientSecret(spec.Key, cfg)
	}
	if clientID == "" || clientSecret == "" {
		return "", "", false
	}
	return clientID, clientSecret, true
}

func defaultClientID(key string) string {
	switch key {
	case "ultimail":
		return "ulti-backend"
	case "nextcloud":
		return "ulti-nextcloud"
	case "onlyoffice":
		return "ulti-onlyoffice"
	case "immich":
		return "ulti-immich"
	case "ultidrive":
		return "ulti-drive"
	default:
		return ""
	}
}

func defaultClientSecret(key string, cfg *config.Config) string {
	switch key {
	case "ultimail":
		return cfg.OIDCClientSecret
	case "nextcloud":
		return secretOrChangeme(cfg.NCOIDCClientSecret)
	case "onlyoffice":
		return secretOrChangeme(cfg.OnlyOfficeOIDCClientSecret)
	case "immich":
		return secretOrChangeme(cfg.ImmichOIDCClientSecret)
	case "ultidrive":
		return secretOrChangeme(cfg.DriveOIDCClientSecret)
	default:
		return "changeme"
	}
}

func secretOrChangeme(s string) string {
	if s != "" {
		return s
	}
	return "changeme"
}
