package unlock

import (
	"errors"
	"fmt"
	"os"
	"time"

	"git.wntrmute.dev/kyle/arca/internal/cryptsetup"
	"git.wntrmute.dev/kyle/arca/internal/udisks"
)

// Result holds the outcome of a successful unlock.
type Result struct {
	Device     *udisks.BlockDevice
	Privileged bool // true if unlock required root (cryptsetup path)
}

// Options configures the unlock behavior.
type Options struct {
	ReadPassphrase func() (string, error)
	KeyfilePath    string
}

// Unlocker tries unlock methods in sequence.
type Unlocker struct {
	client *udisks.Client
	opts   Options
}

// New creates a new Unlocker.
func New(client *udisks.Client, opts Options) *Unlocker {
	return &Unlocker{client: client, opts: opts}
}

// Unlock tries each method in order and returns the result on first success.
func (u *Unlocker) Unlock(dev *udisks.BlockDevice, methods []string) (*Result, error) {
	var errs []error
	for _, method := range methods {
		res, err := u.tryMethod(dev, method)
		if err == nil {
			return res, nil
		}
		errs = append(errs, fmt.Errorf("%s: %w", method, err))
	}
	return nil, fmt.Errorf("all unlock methods failed for %s:\n%w", dev.DevicePath, errors.Join(errs...))
}

func (u *Unlocker) tryMethod(dev *udisks.BlockDevice, method string) (*Result, error) {
	switch method {
	case "passphrase":
		ct, err := u.unlockPassphrase(dev)
		if err != nil {
			return nil, err
		}
		return &Result{Device: ct, Privileged: false}, nil
	case "keyfile":
		ct, err := u.unlockKeyfile(dev)
		if err != nil {
			return nil, err
		}
		return &Result{Device: ct, Privileged: false}, nil
	case "fido2", "tpm2":
		ct, err := u.unlockCryptsetup(dev)
		if err != nil {
			return nil, err
		}
		return &Result{Device: ct, Privileged: true}, nil
	default:
		return nil, fmt.Errorf("unknown unlock method: %s", method)
	}
}

func (u *Unlocker) unlockPassphrase(dev *udisks.BlockDevice) (*udisks.BlockDevice, error) {
	if u.opts.ReadPassphrase == nil {
		return nil, fmt.Errorf("no passphrase reader configured")
	}
	pass, err := u.opts.ReadPassphrase()
	if err != nil {
		return nil, err
	}
	return u.client.Unlock(dev, pass)
}

func (u *Unlocker) unlockKeyfile(dev *udisks.BlockDevice) (*udisks.BlockDevice, error) {
	if u.opts.KeyfilePath == "" {
		return nil, fmt.Errorf("no keyfile configured")
	}
	contents, err := os.ReadFile(u.opts.KeyfilePath)
	if err != nil {
		return nil, fmt.Errorf("reading keyfile: %w", err)
	}
	return u.client.UnlockWithKeyfile(dev, contents)
}

func (u *Unlocker) unlockCryptsetup(dev *udisks.BlockDevice) (*udisks.BlockDevice, error) {
	name := cryptsetup.MapperName(dev.DevicePath)
	if err := cryptsetup.Open(dev.DevicePath, name); err != nil {
		return nil, fmt.Errorf("%w (is the FIDO2/TPM2 key plugged in?)", err)
	}

	// Wait for udisks2 to pick up the new dm device.
	for i := 0; i < 10; i++ {
		ct, err := u.client.CleartextDevice(dev)
		if err == nil {
			return ct, nil
		}
		time.Sleep(200 * time.Millisecond)
	}
	return nil, fmt.Errorf("timed out waiting for udisks2 to discover cleartext device")
}
