package provider

import (
	"context"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"net/url"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/core/identifiers"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
)

type GiteaAppConfigView struct {
	Registered  bool
	InstanceURL string
	DisplayName string
}

type GiteaAppConfig struct {
	ID              string
	WorkspaceID     string
	InstanceURL     string
	APIToken        string
	WebhookSecret   string
	DisplayName     string
	CreatedByUserID string
	CreatedAt       time.Time
	UpdatedAt       time.Time
}

type UpsertGiteaAppConfigParams struct {
	InstanceURL   string
	APIToken      string
	WebhookSecret string
	DisplayName   string
}

// StoredGiteaAppConfigSnapshot is the encrypted, persistence-ready Gitea
// configuration shape used by the App installation approval transaction.
// APIToken and WebhookSecret are credential envelopes, never plaintext.
type StoredGiteaAppConfigSnapshot struct {
	InstanceURL   string
	APIToken      string
	WebhookSecret string
	DisplayName   string
}

func DeriveGiteaConnectionWebhookSecret(sharedSecret string, externalSubjectRef string) string {
	trimmedSecret := strings.TrimSpace(sharedSecret)
	trimmedExternalSubjectRef := strings.ToLower(strings.TrimSpace(externalSubjectRef))
	if trimmedSecret == "" || trimmedExternalSubjectRef == "" {
		return ""
	}
	mac := hmac.New(sha256.New, []byte(trimmedSecret))
	_, _ = mac.Write([]byte("gitea-connection\n"))
	_, _ = mac.Write([]byte(trimmedExternalSubjectRef))
	return hex.EncodeToString(mac.Sum(nil))
}

func (store *Store) GetGiteaAppConfigForUser(ctx context.Context, userID string, workspaceID string) (GiteaAppConfigView, error) {
	if err := store.requireWorkspaceAccess(ctx, userID, workspaceID); err != nil {
		return GiteaAppConfigView{}, err
	}

	row := store.db.QueryRow(ctx, `
SELECT provider_key, display_name
FROM service_provider_configs
WHERE workspace_id = $1
  AND provider_kind = 'gitea'
LIMIT 1
`, strings.TrimSpace(workspaceID))

	view := GiteaAppConfigView{Registered: true}
	if err := row.Scan(&view.InstanceURL, &view.DisplayName); err != nil {
		if coredb.IsNotFound(err) {
			return GiteaAppConfigView{Registered: false}, nil
		}
		return GiteaAppConfigView{}, fmt.Errorf("load gitea app config view: %w", err)
	}
	view.InstanceURL = strings.TrimSpace(view.InstanceURL)
	view.DisplayName = strings.TrimSpace(view.DisplayName)
	return view, nil
}

func (store *Store) LoadGiteaAppConfigForWorkspace(ctx context.Context, workspaceID string) (GiteaAppConfig, bool, error) {
	row := store.db.QueryRow(ctx, `
SELECT
    service_provider_configs.id,
    service_provider_configs.workspace_id,
    service_provider_configs.provider_key,
    COALESCE(JSON_UNQUOTE(JSON_EXTRACT(COALESCE(service_provider_configs.credential_envelope, JSON_OBJECT()), '$.apiToken')), ''),
    COALESCE(JSON_UNQUOTE(JSON_EXTRACT(COALESCE(service_provider_configs.credential_envelope, JSON_OBJECT()), '$.webhookSecret')), ''),
    service_provider_configs.display_name,
    COALESCE(creator.user_id, service_provider_configs.created_by_member_id) AS created_by_user_id,
    service_provider_configs.created_at,
    service_provider_configs.updated_at
FROM service_provider_configs
LEFT JOIN members creator
  ON creator.id = service_provider_configs.created_by_member_id
WHERE service_provider_configs.workspace_id = $1
  AND service_provider_configs.provider_kind = 'gitea'
LIMIT 1
`, strings.TrimSpace(workspaceID))

	config, err := store.scanGiteaAppConfig(row)
	if err != nil {
		if coredb.IsNotFound(err) {
			return GiteaAppConfig{}, false, nil
		}
		return GiteaAppConfig{}, false, fmt.Errorf("load gitea app config: %w", err)
	}
	return config, true, nil
}

// PrepareGiteaAppConfigRequestInTx validates and encrypts one Gitea provider
// proposal while capturing the current raw provider configuration as its base.
// When allowKeep is true, blank secret inputs remain blank in requested so the
// approval card can express "keep existing" without exposing a credential.
func (store *Store) PrepareGiteaAppConfigRequestInTx(
	ctx context.Context,
	tx coredb.Tx,
	workspaceID string,
	params UpsertGiteaAppConfigParams,
	allowKeep bool,
) (StoredGiteaAppConfigSnapshot, bool, StoredGiteaAppConfigSnapshot, error) {
	normalized, err := normalizeGiteaAppConfigParams(params)
	if err != nil {
		return StoredGiteaAppConfigSnapshot{}, false, StoredGiteaAppConfigSnapshot{}, err
	}
	if strings.TrimSpace(normalized.DisplayName) == "" {
		return StoredGiteaAppConfigSnapshot{}, false, StoredGiteaAppConfigSnapshot{}, badRequest("Gitea display name is required.")
	}
	_, base, found, err := store.loadStoredGiteaAppConfigForUpdate(ctx, tx, workspaceID)
	if err != nil {
		return StoredGiteaAppConfigSnapshot{}, false, StoredGiteaAppConfigSnapshot{}, err
	}
	requested, err := store.prepareStoredGiteaAppConfig(found, normalized, allowKeep)
	if err != nil {
		return StoredGiteaAppConfigSnapshot{}, false, StoredGiteaAppConfigSnapshot{}, err
	}
	return base, found, requested, nil
}

// ApplyApprovedGiteaAppConfigInTx atomically compares and applies one encrypted
// Gitea proposal. The caller owns App installation authorization and must invoke
// this inside the same transaction that settles the installation request.
func (store *Store) ApplyApprovedGiteaAppConfigInTx(
	ctx context.Context,
	tx coredb.Tx,
	workspaceID string,
	approvedByUserID string,
	expectedBase StoredGiteaAppConfigSnapshot,
	expectedBaseFound bool,
	requested StoredGiteaAppConfigSnapshot,
	now time.Time,
) error {
	trimmedWorkspaceID := strings.TrimSpace(workspaceID)
	existingID, current, found, err := store.loadStoredGiteaAppConfigForUpdate(ctx, tx, trimmedWorkspaceID)
	if err != nil {
		return err
	}
	if found != expectedBaseFound || (found && !storedGiteaAppConfigEqual(current, expectedBase)) {
		return conflict("Gitea configuration changed. Submit a new configuration request.")
	}

	requested.InstanceURL, err = normalizeGiteaInstanceURL(requested.InstanceURL)
	if err != nil {
		return err
	}
	requested.DisplayName = strings.TrimSpace(requested.DisplayName)
	if requested.DisplayName == "" {
		return badRequest("Gitea display name is required.")
	}
	if strings.TrimSpace(requested.APIToken) == "" {
		if !found {
			return badRequest("Gitea API token is required.")
		}
		requested.APIToken = current.APIToken
	}
	if strings.TrimSpace(requested.WebhookSecret) == "" {
		if !found {
			return badRequest("Gitea webhook secret is required.")
		}
		requested.WebhookSecret = current.WebhookSecret
	}
	if err := store.requirePersistedGiteaAppConfigSecret(requested.APIToken, "Gitea API token is required."); err != nil {
		return err
	}
	if err := store.requirePersistedGiteaAppConfigSecret(requested.WebhookSecret, "Gitea webhook secret is required."); err != nil {
		return err
	}
	if now.IsZero() {
		now = time.Now().UTC()
	} else {
		now = now.UTC()
	}
	return store.upsertStoredGiteaAppConfigInTx(ctx, tx, trimmedWorkspaceID, strings.TrimSpace(approvedByUserID), existingID, found, requested, now)
}

func normalizeGiteaAppConfigParams(params UpsertGiteaAppConfigParams) (UpsertGiteaAppConfigParams, error) {
	instanceURL, err := normalizeGiteaInstanceURL(params.InstanceURL)
	if err != nil {
		return UpsertGiteaAppConfigParams{}, err
	}
	return UpsertGiteaAppConfigParams{
		InstanceURL:   instanceURL,
		APIToken:      strings.TrimSpace(params.APIToken),
		WebhookSecret: strings.TrimSpace(params.WebhookSecret),
		DisplayName:   strings.TrimSpace(params.DisplayName),
	}, nil
}

func (store *Store) prepareStoredGiteaAppConfig(
	found bool,
	params UpsertGiteaAppConfigParams,
	allowKeep bool,
) (StoredGiteaAppConfigSnapshot, error) {
	requested := StoredGiteaAppConfigSnapshot{
		InstanceURL: params.InstanceURL,
		DisplayName: params.DisplayName,
	}
	if params.APIToken != "" {
		encrypted, err := store.encryptGiteaAppConfigSecret(params.APIToken)
		if err != nil {
			return StoredGiteaAppConfigSnapshot{}, err
		}
		requested.APIToken = encrypted
	} else if !allowKeep || !found {
		return StoredGiteaAppConfigSnapshot{}, badRequest("Gitea API token is required.")
	}
	if params.WebhookSecret != "" {
		encrypted, err := store.encryptGiteaAppConfigSecret(params.WebhookSecret)
		if err != nil {
			return StoredGiteaAppConfigSnapshot{}, err
		}
		requested.WebhookSecret = encrypted
	} else if !allowKeep || !found {
		return StoredGiteaAppConfigSnapshot{}, badRequest("Gitea webhook secret is required.")
	}
	return requested, nil
}

func (store *Store) loadStoredGiteaAppConfigForUpdate(ctx context.Context, tx coredb.Tx, workspaceID string) (string, StoredGiteaAppConfigSnapshot, bool, error) {
	var (
		id       string
		snapshot StoredGiteaAppConfigSnapshot
	)
	err := tx.QueryRow(ctx, `
SELECT id, provider_key,
       COALESCE(JSON_UNQUOTE(JSON_EXTRACT(COALESCE(credential_envelope, JSON_OBJECT()), '$.apiToken')), ''),
       COALESCE(JSON_UNQUOTE(JSON_EXTRACT(COALESCE(credential_envelope, JSON_OBJECT()), '$.webhookSecret')), ''),
       display_name
FROM service_provider_configs
WHERE workspace_id = $1
  AND provider_kind = 'gitea'
LIMIT 1
FOR UPDATE
`, strings.TrimSpace(workspaceID)).Scan(&id, &snapshot.InstanceURL, &snapshot.APIToken, &snapshot.WebhookSecret, &snapshot.DisplayName)
	if err == nil {
		snapshot.InstanceURL = strings.TrimSpace(snapshot.InstanceURL)
		snapshot.APIToken = strings.TrimSpace(snapshot.APIToken)
		snapshot.WebhookSecret = strings.TrimSpace(snapshot.WebhookSecret)
		snapshot.DisplayName = strings.TrimSpace(snapshot.DisplayName)
		return strings.TrimSpace(id), snapshot, true, nil
	}
	if coredb.IsNotFound(err) {
		return "", StoredGiteaAppConfigSnapshot{}, false, nil
	}
	return "", StoredGiteaAppConfigSnapshot{}, false, fmt.Errorf("load gitea app config for update: %w", err)
}

func storedGiteaAppConfigEqual(left StoredGiteaAppConfigSnapshot, right StoredGiteaAppConfigSnapshot) bool {
	return strings.TrimSpace(left.InstanceURL) == strings.TrimSpace(right.InstanceURL) &&
		strings.TrimSpace(left.APIToken) == strings.TrimSpace(right.APIToken) &&
		strings.TrimSpace(left.WebhookSecret) == strings.TrimSpace(right.WebhookSecret) &&
		strings.TrimSpace(left.DisplayName) == strings.TrimSpace(right.DisplayName)
}

func (store *Store) upsertStoredGiteaAppConfigInTx(
	ctx context.Context,
	tx coredb.Tx,
	workspaceID string,
	userID string,
	existingID string,
	found bool,
	requested StoredGiteaAppConfigSnapshot,
	now time.Time,
) error {
	trimmedWorkspaceID := strings.TrimSpace(workspaceID)
	creatorMemberID, err := resolveProviderConfigCreatorMemberID(ctx, tx, trimmedWorkspaceID, strings.TrimSpace(userID))
	if err != nil {
		return err
	}
	configJSON, err := giteaProviderConfigJSON(requested.InstanceURL)
	if err != nil {
		return err
	}
	envelopeJSON, err := giteaProviderCredentialEnvelopeJSON(requested.APIToken, requested.WebhookSecret)
	if err != nil {
		return err
	}
	updateExisting := func(id string) error {
		if _, err := tx.Exec(ctx, `
UPDATE service_provider_configs
SET workspace_id = $2,
    provider_key = $3,
    config = $4,
    credential_envelope = $5,
    credential_envelope_version = credential_envelope_version + 1,
    display_name = $6,
    created_by_member_id = $7,
    updated_at = $8
WHERE id = $1
`, id, workspaceID, requested.InstanceURL, configJSON, envelopeJSON, requested.DisplayName, creatorMemberID, now); err != nil {
			return fmt.Errorf("update gitea app config: %w", err)
		}
		return nil
	}
	if found {
		return updateExisting(existingID)
	}
	configID, err := identifiers.NewEntityID("gtcfg")
	if err != nil {
		return fmt.Errorf("generate gitea app config id: %w", err)
	}
	if _, err := tx.Exec(ctx, `
INSERT INTO service_provider_configs (
    id, workspace_id, provider_kind, provider_key, display_name,
    config, credential_envelope, credential_envelope_version,
    created_by_member_id, created_at, updated_at
)
VALUES ($1, $2, 'gitea', $3, $4, $5, $6, 1, $7, $8, $8)
	`, configID, workspaceID, requested.InstanceURL, requested.DisplayName, configJSON, envelopeJSON, creatorMemberID, now); err != nil {
		if coredb.IsUniqueViolation(err) {
			return conflict("Gitea configuration changed. Submit a new configuration request.")
		}
		return fmt.Errorf("insert gitea app config: %w", err)
	}
	return nil
}

func (store *Store) ListGiteaConnectionsForUser(ctx context.Context, userID string, workspaceID string) ([]AppConnectionView, error) {
	if err := store.requireWorkspaceAccess(ctx, userID, workspaceID); err != nil {
		return nil, err
	}

	installation, found, err := store.findAppInstallationByWorkspaceAndApp(ctx, workspaceID, "app_gitea")
	if err != nil {
		return nil, err
	}
	if !found || !installation.Enabled {
		return []AppConnectionView{}, nil
	}
	return store.ListAppConnections(ctx, installation.ID)
}

func (store *Store) scanGiteaAppConfig(row coredb.Row) (GiteaAppConfig, error) {
	var (
		config                 GiteaAppConfig
		encryptedAPIToken      string
		encryptedWebhookSecret string
	)
	if err := row.Scan(
		&config.ID,
		&config.WorkspaceID,
		&config.InstanceURL,
		&encryptedAPIToken,
		&encryptedWebhookSecret,
		&config.DisplayName,
		&config.CreatedByUserID,
		&config.CreatedAt,
		&config.UpdatedAt,
	); err != nil {
		return GiteaAppConfig{}, err
	}

	var err error
	config.APIToken, err = store.decryptGiteaAppConfigSecret(encryptedAPIToken)
	if err != nil {
		return GiteaAppConfig{}, fmt.Errorf("decrypt gitea app api token: %w", err)
	}
	config.WebhookSecret, err = store.decryptGiteaAppConfigSecret(encryptedWebhookSecret)
	if err != nil {
		return GiteaAppConfig{}, fmt.Errorf("decrypt gitea app webhook secret: %w", err)
	}
	config.ID = strings.TrimSpace(config.ID)
	config.WorkspaceID = strings.TrimSpace(config.WorkspaceID)
	config.InstanceURL = strings.TrimSpace(config.InstanceURL)
	config.APIToken = strings.TrimSpace(config.APIToken)
	config.WebhookSecret = strings.TrimSpace(config.WebhookSecret)
	config.DisplayName = strings.TrimSpace(config.DisplayName)
	config.CreatedByUserID = strings.TrimSpace(config.CreatedByUserID)
	return config, nil
}

func (store *Store) requirePersistedGiteaAppConfigSecret(stored string, missingMessage string) error {
	secret, err := store.decryptGiteaAppConfigSecret(stored)
	if err != nil {
		return err
	}
	if strings.TrimSpace(secret) == "" {
		return badRequest(missingMessage)
	}
	return nil
}

func normalizeGiteaInstanceURL(raw string) (string, error) {
	trimmed := strings.TrimSpace(raw)
	if trimmed == "" {
		return "", badRequest("Gitea instance URL is required.")
	}

	parsed, err := url.Parse(trimmed)
	if err != nil || strings.TrimSpace(parsed.Host) == "" {
		return "", badRequest("Gitea instance URL must be a valid absolute URL.")
	}
	if !strings.EqualFold(parsed.Scheme, "http") && !strings.EqualFold(parsed.Scheme, "https") {
		return "", badRequest("Gitea instance URL must use http or https.")
	}
	if parsed.User != nil {
		return "", badRequest("Gitea instance URL must not contain credentials.")
	}
	parsed.RawQuery = ""
	parsed.Fragment = ""
	return strings.TrimRight(parsed.String(), "/"), nil
}
