package server

import (
	"context"
	"log"

	ct "git.autistici.org/ai3/go-common/ldap/compositetypes"
	"gopkg.in/yaml.v3"

	"git.autistici.org/id/auth/backend"
)

// BackendSpec parameters for the file backend.
type fileServiceParams struct {
	Src string `yaml:"src"`
}

type fileUser struct {
	Name              string   `yaml:"name"`
	Email             string   `yaml:"email"`
	Shard             string   `yaml:"shard"`
	EncryptedPassword string   `yaml:"password"`
	TOTPSecret        string   `yaml:"totp_secret"`
	Groups            []string `yaml:"groups"`

	// Legacy U2F registrations are encoded in a similar format as
	// the one produced by old versions of 'pamu2fcfg': the key
	// handle is base64-encoded (this is "websafe" base64, without
	// padding), the public key is hex encoded.
	U2FRegistrations []struct {
		KeyHandle string `yaml:"key_handle"`
		PublicKey string `yaml:"public_key"`
		Comment   string `yaml:"comment"`
	} `yaml:"u2f_registrations"`

	// WebAuthN registrations are encoded as emitted by modern
	// versions of pamu2fcfg: both values are base64-encoded
	// (standard, with padding). The key is actually in COSE format.
	WebAuthNRegistrations []struct {
		KeyHandle string `yaml:"key_handle"`
		PublicKey string `yaml:"public_key"`
		Comment   string `yaml:"comment"`
	} `yaml:"webauthn_registrations"`

	AppSpecificPasswords []struct {
		ID                string `yaml:"id"`
		Service           string `yaml:"service"`
		EncryptedPassword string `yaml:"password"`
		Comment           string `yaml:"comment"`
	} `yaml:"app_specific_passwords"`
}

func (f *fileUser) toUser(filename string) *backend.User {
	u := &backend.User{
		Name:              f.Name,
		Email:             f.Email,
		Shard:             f.Shard,
		EncryptedPassword: f.EncryptedPassword,
		TOTPSecret:        f.TOTPSecret,
		Groups:            f.Groups,
	}

	for _, asp := range f.AppSpecificPasswords {
		u.AppSpecificPasswords = append(u.AppSpecificPasswords, &backend.AppSpecificPassword{
			ID:                asp.ID,
			Service:           asp.Service,
			EncryptedPassword: asp.EncryptedPassword,
		})
	}

	for _, r := range f.WebAuthNRegistrations {
		reg, err := ct.ParseU2FRegistrationFromStrings(r.KeyHandle, r.PublicKey)
		if err != nil {
			log.Printf("warning: %s: user %s: could not decode WebAuthN registration: %v", filename, f.Name, err)
			continue
		}

		cred, err := reg.Decode()
		if err != nil {
			log.Printf("warning: %s: user %s: could not decode WebAuthN registration: %v", filename, f.Name, err)
			continue
		}
		u.WebAuthnRegistrations = append(u.WebAuthnRegistrations, cred)
	}

	for _, r := range f.U2FRegistrations {
		reg, err := ct.ParseLegacyU2FRegistrationFromStrings(r.KeyHandle, r.PublicKey)
		if err != nil {
			log.Printf("warning: %s: user %s: could not decode U2F registration: %v", filename, f.Name, err)
			continue
		}

		cred, err := reg.Decode()
		if err != nil {
			log.Printf("warning: %s: user %s: could not decode U2F registration: %v", filename, f.Name, err)
			continue
		}
		u.WebAuthnRegistrations = append(u.WebAuthnRegistrations, cred)
	}

	return u
}

// Simple file-based authentication backend, list users and their
// credentials in a YAML-encoded file.
type fileBackend struct {
	files     map[string]map[string]*backend.User
	configDir string
}

func loadUsersFile(path string) (map[string]*backend.User, error) {
	var userList []*fileUser
	if err := backend.LoadYAML(path, &userList); err != nil {
		return nil, err
	}
	users := make(map[string]*backend.User)
	for _, u := range userList {
		users[u.Name] = u.toUser(path)
	}
	return users, nil
}

// New creates a new file-based UserBackend.
func New(_ *yaml.Node, configDir string) (backend.UserBackend, error) {
	return &fileBackend{
		files:     make(map[string]map[string]*backend.User),
		configDir: configDir,
	}, nil
}

func (b *fileBackend) getUserMap(path string) (map[string]*backend.User, error) {
	m, ok := b.files[path]
	if !ok {
		var err error
		m, err = loadUsersFile(path)
		if err != nil {
			return nil, err
		}
		b.files[path] = m
	}
	return m, nil
}

func (b *fileBackend) Close() {}

func (b *fileBackend) NewServiceBackend(spec *backend.Spec) (backend.ServiceBackend, error) {
	var params fileServiceParams
	if err := spec.Params.Decode(&params); err != nil {
		return nil, err
	}
	m, err := b.getUserMap(backend.ResolvePath(params.Src, b.configDir))
	if err != nil {
		return nil, err
	}
	return fileServiceBackend(m), nil
}

type fileServiceBackend map[string]*backend.User

func (b fileServiceBackend) GetUser(_ context.Context, name string) (*backend.User, bool) {
	u, ok := b[name]
	return u, ok
}