package auth

import (
	"context"
	"crypto/rand"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"sync"
	"time"

	"github.com/go-webauthn/webauthn/protocol"
	"github.com/go-webauthn/webauthn/webauthn"

	"github.com/operator/command-center/internal/secrets"
)

// CeremonySessionCookie is the short-lived cookie that ties together
// /register/start ↔ /register/finish and /login/start ↔ /login/finish. It
// holds an opaque random token that maps to webauthn.SessionData in memory.
const CeremonySessionCookie = "cc_webauthn_ceremony"

// ceremonySessionLifetime is how long the operator has to complete a WebAuthn
// ceremony after Start. 5 minutes is well above human reaction time and well
// below the lifetime of any reasonable browser tab.
const ceremonySessionLifetime = 5 * time.Minute

// WebAuthn wraps the library plus the credential/session stores and the
// in-memory ceremony state map.
type WebAuthn struct {
	wa      *webauthn.WebAuthn
	creds   *CredentialStore
	secrets *secrets.Store

	mu        sync.Mutex
	ceremony  map[string]ceremonyState
}

type ceremonyState struct {
	session webauthn.SessionData
	expires time.Time
}

// Config tunes the relying-party parameters. Empty fields receive sensible
// defaults; production deployments should set RPID and RPOrigins explicitly
// to the Tailscale hostname.
type Config struct {
	RPID          string
	RPDisplayName string
	RPOrigins     []string
}

// New constructs a WebAuthn from its dependencies. Returns an error if the
// underlying library rejects the config.
func New(cfg Config, creds *CredentialStore, secretsStore *secrets.Store) (*WebAuthn, error) {
	if cfg.RPID == "" {
		cfg.RPID = "localhost"
	}
	if cfg.RPDisplayName == "" {
		cfg.RPDisplayName = "Seedbox Command Center"
	}
	if len(cfg.RPOrigins) == 0 {
		cfg.RPOrigins = []string{"http://localhost:8443", "http://127.0.0.1:8443"}
	}
	wa, err := webauthn.New(&webauthn.Config{
		RPID:          cfg.RPID,
		RPDisplayName: cfg.RPDisplayName,
		RPOrigins:     cfg.RPOrigins,
	})
	if err != nil {
		return nil, fmt.Errorf("auth: webauthn config: %w", err)
	}
	return &WebAuthn{
		wa:       wa,
		creds:    creds,
		secrets:  secretsStore,
		ceremony: map[string]ceremonyState{},
	}, nil
}

// BeginRegistration starts a registration ceremony. The returned JSON is
// what the browser passes to startRegistration() from @simplewebauthn/browser.
// A short-lived cookie ties this ceremony to the subsequent /finish call.
func (w *WebAuthn) BeginRegistration(ctx context.Context, rw http.ResponseWriter) ([]byte, error) {
	user, err := LoadOrCreateUser(ctx, w.secrets, w.creds)
	if err != nil {
		return nil, err
	}

	options, sessionData, err := w.wa.BeginRegistration(user)
	if err != nil {
		return nil, fmt.Errorf("auth: begin registration: %w", err)
	}

	if err := w.storeCeremony(rw, *sessionData); err != nil {
		return nil, err
	}
	return json.Marshal(options)
}

// FinishRegistration completes the registration ceremony. On success the
// new credential is persisted and the caller proceeds to recovery-code
// generation and session issuance.
func (w *WebAuthn) FinishRegistration(ctx context.Context, r *http.Request, deviceName string) (*webauthn.Credential, error) {
	user, err := LoadOrCreateUser(ctx, w.secrets, w.creds)
	if err != nil {
		return nil, err
	}
	sess, err := w.loadCeremony(r)
	if err != nil {
		return nil, err
	}
	cred, err := w.wa.FinishRegistration(user, sess, r)
	if err != nil {
		return nil, fmt.Errorf("auth: finish registration: %w", err)
	}
	if err := w.creds.Insert(ctx, cred, deviceName); err != nil {
		return nil, err
	}
	return cred, nil
}

// BeginLogin starts an assertion ceremony.
func (w *WebAuthn) BeginLogin(ctx context.Context, rw http.ResponseWriter) ([]byte, error) {
	user, err := LoadOrCreateUser(ctx, w.secrets, w.creds)
	if err != nil {
		return nil, err
	}
	if len(user.WebAuthnCredentials()) == 0 {
		return nil, errors.New("auth: no credentials registered")
	}
	options, sessionData, err := w.wa.BeginLogin(user)
	if err != nil {
		return nil, fmt.Errorf("auth: begin login: %w", err)
	}
	if err := w.storeCeremony(rw, *sessionData); err != nil {
		return nil, err
	}
	return json.Marshal(options)
}

// FinishLogin completes the assertion ceremony, updates the credential's
// counter, and returns the credential that matched. The caller issues a
// session cookie.
func (w *WebAuthn) FinishLogin(ctx context.Context, r *http.Request) (*webauthn.Credential, error) {
	user, err := LoadOrCreateUser(ctx, w.secrets, w.creds)
	if err != nil {
		return nil, err
	}
	sess, err := w.loadCeremony(r)
	if err != nil {
		return nil, err
	}
	cred, err := w.wa.FinishLogin(user, sess, r)
	if err != nil {
		return nil, fmt.Errorf("auth: finish login: %w", err)
	}
	if err := w.creds.UpdateCounter(ctx, cred.ID, cred.Authenticator.SignCount); err != nil {
		return nil, err
	}
	return cred, nil
}

func (w *WebAuthn) storeCeremony(rw http.ResponseWriter, session webauthn.SessionData) error {
	token := make([]byte, 16)
	if _, err := rand.Read(token); err != nil {
		return err
	}
	key := base64.RawURLEncoding.EncodeToString(token)

	w.mu.Lock()
	w.gcLocked()
	w.ceremony[key] = ceremonyState{session: session, expires: time.Now().Add(ceremonySessionLifetime)}
	w.mu.Unlock()

	http.SetCookie(rw, &http.Cookie{
		Name:     CeremonySessionCookie,
		Value:    key,
		Path:     "/api/auth/webauthn/",
		Expires:  time.Now().Add(ceremonySessionLifetime),
		HttpOnly: true,
		Secure:   true,
		SameSite: http.SameSiteStrictMode,
	})
	return nil
}

func (w *WebAuthn) loadCeremony(r *http.Request) (webauthn.SessionData, error) {
	c, err := r.Cookie(CeremonySessionCookie)
	if err != nil || c.Value == "" {
		return webauthn.SessionData{}, errors.New("auth: no ceremony session — start a fresh ceremony")
	}
	w.mu.Lock()
	defer w.mu.Unlock()
	state, ok := w.ceremony[c.Value]
	if !ok || time.Now().After(state.expires) {
		delete(w.ceremony, c.Value)
		return webauthn.SessionData{}, errors.New("auth: ceremony session expired — start a fresh ceremony")
	}
	delete(w.ceremony, c.Value) // single-use
	return state.session, nil
}

func (w *WebAuthn) gcLocked() {
	now := time.Now()
	for k, v := range w.ceremony {
		if now.After(v.expires) {
			delete(w.ceremony, k)
		}
	}
}

// PartialAttestationResponse mirrors the smallest subset of the assertion
// JSON we want to validate for shape before passing to the library. The
// library re-parses; this is only to fail-fast on grossly malformed input
// and to extract the credential id when needed (e.g., for protocol-level
// audit logging). Currently unused; kept here for documented intent.
type partialAttestation struct {
	ID    string `json:"id"`
	RawID string `json:"rawId"`
}

// Unused vars referenced via documentation; the silencer keeps go vet happy.
var _ protocol.AuthenticatorTransport = ""
var _ partialAttestation
