package security

import (
	"context"
	"crypto/rand"
	"database/sql"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"sync"
	"time"
)

// DatabasePasskeyProvider implements PasskeyProvider using database storage
// Procedure names are configurable via SQLNames (see DefaultSQLNames for defaults)
type DatabasePasskeyProvider struct {
	db         *sql.DB
	dbMu       sync.RWMutex
	dbFactory  func() (*sql.DB, error)
	rpID       string // Relying Party ID (domain)
	rpName     string // Relying Party display name
	rpOrigin   string // Expected origin for WebAuthn
	timeout    int64  // Timeout in milliseconds (default: 60000)
	sqlNames   *SQLNames
	tableNames *TableNames
	queryMode  QueryMode
	capability *dbCapability
}

// DatabasePasskeyProviderOptions configures the passkey provider
type DatabasePasskeyProviderOptions struct {
	// RPID is the Relying Party ID (typically your domain, e.g., "example.com")
	RPID string
	// RPName is the display name for your relying party
	RPName string
	// RPOrigin is the expected origin (e.g., "https://example.com")
	RPOrigin string
	// Timeout is the timeout for operations in milliseconds (default: 60000)
	Timeout int64
	// SQLNames provides custom SQL procedure/function names. If nil, uses DefaultSQLNames().
	SQLNames *SQLNames
	// TableNames provides custom table names for Direct mode. If nil, uses DefaultTableNames().
	TableNames *TableNames
	// QueryMode selects stored-procedure vs Direct-mode SQL. Default (zero value) is ModeAuto.
	QueryMode QueryMode
	// DBFactory is called to obtain a fresh *sql.DB when the existing connection is closed.
	// If nil, reconnection is disabled.
	DBFactory func() (*sql.DB, error)
}

// NewDatabasePasskeyProvider creates a new database-backed passkey provider
func NewDatabasePasskeyProvider(db *sql.DB, opts DatabasePasskeyProviderOptions) *DatabasePasskeyProvider {
	if opts.Timeout == 0 {
		opts.Timeout = 60000 // 60 seconds default
	}

	sqlNames := MergeSQLNames(DefaultSQLNames(), opts.SQLNames)
	tableNames := resolveTableNames(opts.TableNames)

	return &DatabasePasskeyProvider{
		db:         db,
		dbFactory:  opts.DBFactory,
		rpID:       opts.RPID,
		rpName:     opts.RPName,
		rpOrigin:   opts.RPOrigin,
		timeout:    opts.Timeout,
		sqlNames:   sqlNames,
		tableNames: tableNames,
		queryMode:  opts.QueryMode,
		capability: newDBCapability(),
	}
}

func (p *DatabasePasskeyProvider) getDB() *sql.DB {
	p.dbMu.RLock()
	defer p.dbMu.RUnlock()
	return p.db
}

func (p *DatabasePasskeyProvider) reconnectDB() error {
	if p.dbFactory == nil {
		return fmt.Errorf("no db factory configured for reconnect")
	}
	newDB, err := p.dbFactory()
	if err != nil {
		return err
	}
	p.dbMu.Lock()
	p.db = newDB
	p.dbMu.Unlock()
	if p.capability != nil {
		p.capability.reset()
	}
	return nil
}

func (p *DatabasePasskeyProvider) runDBOpWithReconnect(run func(*sql.DB) error) error {
	db := p.getDB()
	if db == nil {
		return fmt.Errorf("database connection is nil")
	}
	err := run(db)
	if isDBClosed(err) {
		if reconnErr := p.reconnectDB(); reconnErr == nil {
			err = run(p.getDB())
		}
	}
	return err
}

// BeginRegistration creates registration options for a new passkey
func (p *DatabasePasskeyProvider) BeginRegistration(ctx context.Context, userID int, username, displayName string) (*PasskeyRegistrationOptions, error) {
	// Generate challenge
	challenge := make([]byte, 32)
	if _, err := rand.Read(challenge); err != nil {
		return nil, fmt.Errorf("failed to generate challenge: %w", err)
	}

	// Get existing credentials to exclude
	credentials, err := p.GetCredentials(ctx, userID)
	if err != nil {
		return nil, fmt.Errorf("failed to get existing credentials: %w", err)
	}

	excludeCredentials := make([]PasskeyCredentialDescriptor, 0, len(credentials))
	for i := range credentials {
		excludeCredentials = append(excludeCredentials, PasskeyCredentialDescriptor{
			Type:       "public-key",
			ID:         credentials[i].CredentialID,
			Transports: credentials[i].Transports,
		})
	}

	// Create user handle (persistent user ID)
	userHandle := []byte(fmt.Sprintf("user_%d", userID))

	return &PasskeyRegistrationOptions{
		Challenge: challenge,
		RelyingParty: PasskeyRelyingParty{
			ID:   p.rpID,
			Name: p.rpName,
		},
		User: PasskeyUser{
			ID:          userHandle,
			Name:        username,
			DisplayName: displayName,
		},
		PubKeyCredParams: []PasskeyCredentialParam{
			{Type: "public-key", Alg: -7},   // ES256 (ECDSA with SHA-256)
			{Type: "public-key", Alg: -257}, // RS256 (RSASSA-PKCS1-v1_5 with SHA-256)
		},
		Timeout:            p.timeout,
		ExcludeCredentials: excludeCredentials,
		AuthenticatorSelection: &PasskeyAuthenticatorSelection{
			RequireResidentKey: false,
			ResidentKey:        "preferred",
			UserVerification:   "preferred",
		},
		Attestation: "none",
	}, nil
}

// CompleteRegistration verifies and stores a new passkey credential
// NOTE: This is a simplified implementation. In production, you should use a WebAuthn library
// like github.com/go-webauthn/webauthn to properly verify attestation and parse credentials.
func (p *DatabasePasskeyProvider) CompleteRegistration(ctx context.Context, userID int, response PasskeyRegistrationResponse, expectedChallenge []byte) (*PasskeyCredential, error) {
	// TODO: Implement full WebAuthn verification
	// 1. Verify clientDataJSON contains correct challenge and origin
	// 2. Parse and verify attestationObject
	// 3. Extract public key and credential ID
	// 4. Verify attestation signature (if not "none")

	// For now, this is a placeholder that stores the credential data
	// In production, you MUST use a proper WebAuthn library

	credIDB64 := base64.StdEncoding.EncodeToString(response.RawID)
	pubKeyB64 := base64.StdEncoding.EncodeToString(response.Response.AttestationObject)

	if !p.capability.ShouldUseProcedure(ctx, p.queryMode, p.getDB(), p.sqlNames.PasskeyStoreCredential) {
		credentialID, err := p.storeCredentialDirect(ctx, storeCredentialParams{
			UserID:          userID,
			CredentialID:    credIDB64,
			PublicKey:       pubKeyB64,
			AttestationType: "none",
			SignCount:       0,
			Transports:      response.Transports,
			BackupEligible:  false,
			BackupState:     false,
			Name:            "Passkey",
		})
		if err != nil {
			return nil, err
		}
		return &PasskeyCredential{
			ID:              fmt.Sprintf("%d", credentialID),
			UserID:          userID,
			CredentialID:    response.RawID,
			PublicKey:       response.Response.AttestationObject,
			AttestationType: "none",
			Transports:      response.Transports,
			CreatedAt:       time.Now(),
			LastUsedAt:      time.Now(),
		}, nil
	}

	credData := map[string]any{
		"user_id":          userID,
		"credential_id":    credIDB64,
		"public_key":       pubKeyB64,
		"attestation_type": "none",
		"sign_count":       0,
		"transports":       response.Transports,
		"backup_eligible":  false,
		"backup_state":     false,
		"name":             "Passkey",
	}

	credJSON, err := json.Marshal(credData)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal credential data: %w", err)
	}

	var success bool
	var errorMsg sql.NullString
	var credentialID sql.NullInt64

	query := fmt.Sprintf(`SELECT p_success, p_error, p_credential_id FROM %s($1::jsonb)`, p.sqlNames.PasskeyStoreCredential)
	err = p.getDB().QueryRowContext(ctx, query, string(credJSON)).Scan(&success, &errorMsg, &credentialID)
	if err != nil {
		return nil, fmt.Errorf("failed to store credential: %w", err)
	}

	if !success {
		if errorMsg.Valid {
			return nil, fmt.Errorf("%s", errorMsg.String)
		}
		return nil, fmt.Errorf("failed to store credential")
	}

	return &PasskeyCredential{
		ID:              fmt.Sprintf("%d", credentialID.Int64),
		UserID:          userID,
		CredentialID:    response.RawID,
		PublicKey:       response.Response.AttestationObject,
		AttestationType: "none",
		Transports:      response.Transports,
		CreatedAt:       time.Now(),
		LastUsedAt:      time.Now(),
	}, nil
}

// BeginAuthentication creates authentication options for passkey login
func (p *DatabasePasskeyProvider) BeginAuthentication(ctx context.Context, username string) (*PasskeyAuthenticationOptions, error) {
	// Generate challenge
	challenge := make([]byte, 32)
	if _, err := rand.Read(challenge); err != nil {
		return nil, fmt.Errorf("failed to generate challenge: %w", err)
	}

	// If username is provided, get user's credentials
	var allowCredentials []PasskeyCredentialDescriptor
	if username != "" {
		var creds []passkeyCredential

		if !p.capability.ShouldUseProcedure(ctx, p.queryMode, p.getDB(), p.sqlNames.PasskeyGetCredsByUsername) {
			_, directCreds, err := p.getCredsByUsernameDirect(ctx, username)
			if err != nil {
				return nil, err
			}
			creds = directCreds
		} else {
			var success bool
			var errorMsg sql.NullString
			var userID sql.NullInt64
			var credentialsJSON sql.NullString

			query := fmt.Sprintf(`SELECT p_success, p_error, p_user_id, p_credentials::text FROM %s($1)`, p.sqlNames.PasskeyGetCredsByUsername)
			err := p.getDB().QueryRowContext(ctx, query, username).Scan(&success, &errorMsg, &userID, &credentialsJSON)
			if err != nil {
				return nil, fmt.Errorf("failed to get credentials: %w", err)
			}

			if !success {
				if errorMsg.Valid {
					return nil, fmt.Errorf("%s", errorMsg.String)
				}
				return nil, fmt.Errorf("failed to get credentials")
			}

			if err := json.Unmarshal([]byte(credentialsJSON.String), &creds); err != nil {
				return nil, fmt.Errorf("failed to parse credentials: %w", err)
			}
		}

		allowCredentials = make([]PasskeyCredentialDescriptor, 0, len(creds))
		for _, cred := range creds {
			credID, err := base64.StdEncoding.DecodeString(cred.ID)
			if err != nil {
				continue
			}
			allowCredentials = append(allowCredentials, PasskeyCredentialDescriptor{
				Type:       "public-key",
				ID:         credID,
				Transports: cred.Transports,
			})
		}
	}

	return &PasskeyAuthenticationOptions{
		Challenge:        challenge,
		Timeout:          p.timeout,
		RelyingPartyID:   p.rpID,
		AllowCredentials: allowCredentials,
		UserVerification: "preferred",
	}, nil
}

// CompleteAuthentication verifies a passkey assertion and returns the user ID
// NOTE: This is a simplified implementation. In production, you should use a WebAuthn library
// like github.com/go-webauthn/webauthn to properly verify the assertion signature.
func (p *DatabasePasskeyProvider) CompleteAuthentication(ctx context.Context, response PasskeyAuthenticationResponse, expectedChallenge []byte) (int, error) {
	// TODO: Implement full WebAuthn verification
	// 1. Verify clientDataJSON contains correct challenge and origin
	// 2. Verify authenticatorData
	// 3. Verify signature using stored public key
	// 4. Update sign counter and check for cloning

	credIDB64 := base64.StdEncoding.EncodeToString(response.RawID)

	if !p.capability.ShouldUseProcedure(ctx, p.queryMode, p.getDB(), p.sqlNames.PasskeyGetCredential) {
		userID, signCount, err := p.getCredentialDirect(ctx, credIDB64)
		if err != nil {
			return 0, err
		}
		newCounter := signCount + 1
		cloneWarning, err := p.updateCounterDirect(ctx, credIDB64, newCounter)
		if err != nil {
			return 0, fmt.Errorf("failed to update counter: %w", err)
		}
		if cloneWarning {
			return 0, fmt.Errorf("credential cloning detected")
		}
		return userID, nil
	}

	// Get credential from database
	var success bool
	var errorMsg sql.NullString
	var credentialJSON sql.NullString

	runQuery := func() error {
		query := fmt.Sprintf(`SELECT p_success, p_error, p_credential::text FROM %s($1)`, p.sqlNames.PasskeyGetCredential)
		return p.getDB().QueryRowContext(ctx, query, response.RawID).Scan(&success, &errorMsg, &credentialJSON)
	}
	err := runQuery()
	if isDBClosed(err) {
		if reconnErr := p.reconnectDB(); reconnErr == nil {
			err = runQuery()
		}
	}
	if err != nil {
		return 0, fmt.Errorf("failed to get credential: %w", err)
	}

	if !success {
		if errorMsg.Valid {
			return 0, fmt.Errorf("%s", errorMsg.String)
		}
		return 0, fmt.Errorf("credential not found")
	}

	// Parse credential
	var cred struct {
		UserID    int    `json:"user_id"`
		SignCount uint32 `json:"sign_count"`
	}
	if err := json.Unmarshal([]byte(credentialJSON.String), &cred); err != nil {
		return 0, fmt.Errorf("failed to parse credential: %w", err)
	}

	// TODO: Verify signature here
	// For now, we'll just update the counter as a placeholder

	// Update counter (in production, this should be done after successful verification)
	newCounter := cred.SignCount + 1
	var updateSuccess bool
	var updateError sql.NullString
	var cloneWarning sql.NullBool

	updateQuery := fmt.Sprintf(`SELECT p_success, p_error, p_clone_warning FROM %s($1, $2)`, p.sqlNames.PasskeyUpdateCounter)
	err = p.getDB().QueryRowContext(ctx, updateQuery, response.RawID, newCounter).Scan(&updateSuccess, &updateError, &cloneWarning)
	if err != nil {
		return 0, fmt.Errorf("failed to update counter: %w", err)
	}

	if cloneWarning.Valid && cloneWarning.Bool {
		return 0, fmt.Errorf("credential cloning detected")
	}

	return cred.UserID, nil
}

// GetCredentials returns all passkey credentials for a user
func (p *DatabasePasskeyProvider) GetCredentials(ctx context.Context, userID int) ([]PasskeyCredential, error) {
	if !p.capability.ShouldUseProcedure(ctx, p.queryMode, p.getDB(), p.sqlNames.PasskeyGetUserCredentials) {
		return p.getUserCredentialsDirect(ctx, userID)
	}

	var success bool
	var errorMsg sql.NullString
	var credentialsJSON sql.NullString

	query := fmt.Sprintf(`SELECT p_success, p_error, p_credentials::text FROM %s($1)`, p.sqlNames.PasskeyGetUserCredentials)
	err := p.getDB().QueryRowContext(ctx, query, userID).Scan(&success, &errorMsg, &credentialsJSON)
	if err != nil {
		return nil, fmt.Errorf("failed to get credentials: %w", err)
	}

	if !success {
		if errorMsg.Valid {
			return nil, fmt.Errorf("%s", errorMsg.String)
		}
		return nil, fmt.Errorf("failed to get credentials")
	}

	// Parse credentials
	var rawCreds []struct {
		ID              int       `json:"id"`
		UserID          int       `json:"user_id"`
		CredentialID    string    `json:"credential_id"`
		PublicKey       string    `json:"public_key"`
		AttestationType string    `json:"attestation_type"`
		AAGUID          string    `json:"aaguid"`
		SignCount       uint32    `json:"sign_count"`
		CloneWarning    bool      `json:"clone_warning"`
		Transports      []string  `json:"transports"`
		BackupEligible  bool      `json:"backup_eligible"`
		BackupState     bool      `json:"backup_state"`
		Name            string    `json:"name"`
		CreatedAt       time.Time `json:"created_at"`
		LastUsedAt      time.Time `json:"last_used_at"`
	}

	if err := json.Unmarshal([]byte(credentialsJSON.String), &rawCreds); err != nil {
		return nil, fmt.Errorf("failed to parse credentials: %w", err)
	}

	credentials := make([]PasskeyCredential, 0, len(rawCreds))
	for i := range rawCreds {
		raw := rawCreds[i]
		credID, err := base64.StdEncoding.DecodeString(raw.CredentialID)
		if err != nil {
			continue
		}
		pubKey, err := base64.StdEncoding.DecodeString(raw.PublicKey)
		if err != nil {
			continue
		}
		aaguid, _ := base64.StdEncoding.DecodeString(raw.AAGUID)

		credentials = append(credentials, PasskeyCredential{
			ID:              fmt.Sprintf("%d", raw.ID),
			UserID:          raw.UserID,
			CredentialID:    credID,
			PublicKey:       pubKey,
			AttestationType: raw.AttestationType,
			AAGUID:          aaguid,
			SignCount:       raw.SignCount,
			CloneWarning:    raw.CloneWarning,
			Transports:      raw.Transports,
			BackupEligible:  raw.BackupEligible,
			BackupState:     raw.BackupState,
			Name:            raw.Name,
			CreatedAt:       raw.CreatedAt,
			LastUsedAt:      raw.LastUsedAt,
		})
	}

	return credentials, nil
}

// DeleteCredential removes a passkey credential
func (p *DatabasePasskeyProvider) DeleteCredential(ctx context.Context, userID int, credentialID string) error {
	credID, err := base64.StdEncoding.DecodeString(credentialID)
	if err != nil {
		return fmt.Errorf("invalid credential ID: %w", err)
	}

	if !p.capability.ShouldUseProcedure(ctx, p.queryMode, p.getDB(), p.sqlNames.PasskeyDeleteCredential) {
		return p.deleteCredentialDirect(ctx, userID, credentialID)
	}

	var success bool
	var errorMsg sql.NullString

	query := fmt.Sprintf(`SELECT p_success, p_error FROM %s($1, $2)`, p.sqlNames.PasskeyDeleteCredential)
	err = p.getDB().QueryRowContext(ctx, query, userID, credID).Scan(&success, &errorMsg)
	if err != nil {
		return fmt.Errorf("failed to delete credential: %w", err)
	}

	if !success {
		if errorMsg.Valid {
			return fmt.Errorf("%s", errorMsg.String)
		}
		return fmt.Errorf("failed to delete credential")
	}

	return nil
}

// UpdateCredentialName updates the friendly name of a credential
func (p *DatabasePasskeyProvider) UpdateCredentialName(ctx context.Context, userID int, credentialID string, name string) error {
	credID, err := base64.StdEncoding.DecodeString(credentialID)
	if err != nil {
		return fmt.Errorf("invalid credential ID: %w", err)
	}

	if !p.capability.ShouldUseProcedure(ctx, p.queryMode, p.getDB(), p.sqlNames.PasskeyUpdateName) {
		return p.updateNameDirect(ctx, userID, credentialID, name)
	}

	var success bool
	var errorMsg sql.NullString

	query := fmt.Sprintf(`SELECT p_success, p_error FROM %s($1, $2, $3)`, p.sqlNames.PasskeyUpdateName)
	err = p.getDB().QueryRowContext(ctx, query, userID, credID, name).Scan(&success, &errorMsg)
	if err != nil {
		return fmt.Errorf("failed to update credential name: %w", err)
	}

	if !success {
		if errorMsg.Valid {
			return fmt.Errorf("%s", errorMsg.String)
		}
		return fmt.Errorf("failed to update credential name")
	}

	return nil
}
