package client

import (
	"context"
	"crypto/ed25519"
	"crypto/rand"
	"encoding/base64"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"net"
	"os"

	"koti.casa/numenor-labs/dsfx/internal/lib/crypto/identity"
	"koti.casa/numenor-labs/dsfx/internal/lib/disk"
	"koti.casa/numenor-labs/dsfx/internal/lib/logging"
	"koti.casa/numenor-labs/dsfx/internal/lib/network"
	"koti.casa/numenor-labs/dsfx/internal/lib/storage/scoped"
	"koti.casa/numenor-labs/dsfx/internal/lib/system"
)

const (
	// DefaultConfigDir is the default directory for the dsfxctl configuration.
	DefaultConfigDir = "/etc/dsfxctl"
)

// Conf holds the configuration for the dsfxctl application.
type Conf struct {
	// Directories
	ConfigDir string
	LogLevel  string
}

// SlogLevel returns the appropriate slog.Level based on the LogLevel string.
func (c Conf) SlogLevel() slog.Level {
	switch c.LogLevel {
	case "debug":
		return slog.LevelDebug
	case "info":
		return slog.LevelInfo
	case "warn":
		return slog.LevelWarn
	case "error":
		return slog.LevelError
	default:
		return slog.LevelInfo
	}
}

func loadConfigFromSystem(sys system.System) Conf {
	var c Conf

	c.ConfigDir = sys.GetEnv("DSFXCTL_CONFIG_DIR")
	if c.ConfigDir == "" {
		c.ConfigDir = DefaultConfigDir
	}

	// defaults are handled by Conf.SlogLevel.
	c.LogLevel = sys.GetEnv("DSFXCTL_LOG_LEVEL")

	return c
}

// Client represents the client application for dsfxctl.
type Client struct {
	// resources
	disk   disk.Disk
	system system.System
	// configuration
	conf Conf
	// storage scopes
	configScope disk.Disk
}

// New creates a new Client instance with the provided disk, system, and
// configuration.
func New(disk disk.Disk, system system.System) *Client {
	conf := loadConfigFromSystem(system)

	return &Client{
		// resources
		disk:   disk,
		system: system,
		// configuration
		conf: conf,
		// storage scopes
		configScope: scoped.New(disk, conf.ConfigDir),
	}
}

// Run executes the main logic of the application.
func (a *Client) Run(ctx context.Context) error {
	// ---------------------------------------------------------------------------
	// Logger

	opts := &slog.HandlerOptions{
		AddSource: false,
		Level:     a.conf.SlogLevel(),
	}
	logger := slog.New(slog.NewTextHandler(a.system.Stdout(), opts))

	err := a.disk.MkdirAll(a.conf.ConfigDir, 0755)
	if err != nil {
		logger.ErrorContext(ctx, "failed to create config directory", slog.Any("error", err))
		return err
	}

	// Everything in the application will attempt to use the logger in stored in
	// the context, but we also set the default with slog as a fallback. In cases
	// where the context is not available, or the context is not a child of the
	// context with the logger, the default logger will be used.
	slog.SetDefault(logger)
	ctx = logging.WithContext(ctx, logger)

	id, err := a.loadIdentity()
	if err != nil {
		logger.ErrorContext(ctx, "failed to load identity", slog.Any("error", err))
		return err
	}

	laddr := network.NewAddr(
		net.ParseIP("0.0.0.0"),
		0, // port 0 means any available port
		identity.ToPublicKey(id),
	)

	logger.DebugContext(ctx, "using addr", slog.String("address", laddr.String()))

	switch a.system.Arg(0) {
	case "test":
		raddrRaw := a.system.Arg(1)
		if raddrRaw == "" {
			logger.ErrorContext(ctx, "no remote address provided")
			return err
		}
		testConnection(ctx, id, laddr, raddrRaw)
		return nil
	case "identity":
		pubKey := identity.ToPublicKey(id)
		logger.InfoContext(ctx, "identity", slog.String("publicKey", base64.StdEncoding.EncodeToString(pubKey)))
		return nil
	case "":
		return errors.New("no command provided")
	default:
		return errors.New("unknown command: " + a.system.Arg(0))
	}
}

func testConnection(ctx context.Context, id ed25519.PrivateKey, laddr *network.Addr, raddrRaw string) {
	logger := logging.FromContext(context.Background())

	raddr, err := network.ParseAddr(raddrRaw)
	if err != nil {
		logger.ErrorContext(ctx, "failed to parse server address", slog.Any("error", err))
		return
	}

	conn, err := network.Dial(ctx, id, laddr, raddr)
	if err != nil {
		logger.ErrorContext(ctx, "failed to connect", slog.Any("error", err))
		return
	}
	defer conn.Close()

	logger.InfoContext(ctx, "connected to server", slog.String("remote", raddr.String()))
}

// loadIdentity ...
func (c *Client) loadIdentity() (ed25519.PrivateKey, error) {
	hasKeyFile, err := c.hasKeyFile()
	if err != nil {
		return nil, fmt.Errorf("failed to check for admins file: %w", err)
	}

	if !hasKeyFile {
		if err := c.createKeyFile(); err != nil {
			return nil, fmt.Errorf("failed to create admins file: %w", err)
		}
	}

	return c.readKeyFile()
}

// hasKeyFile ...
func (c *Client) hasKeyFile() (bool, error) {
	f, err := c.configScope.Open("ed25519.key")
	if errors.Is(err, os.ErrNotExist) {
		return false, nil // Key file does not exist
	}
	if err != nil {
		return false, nil
	}
	defer f.Close()

	return true, nil
}

// createKeyFile ...
func (c *Client) createKeyFile() error {
	f, err := c.configScope.Create("ed25519.key")
	if err != nil {
		return err
	}
	defer f.Close()

	_, privateKey, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		return err
	}

	_, err = f.Write([]byte(base64.StdEncoding.EncodeToString(privateKey)))
	if err != nil {
		return err
	}

	return nil
}

// readKeyFile ...
func (c *Client) readKeyFile() (ed25519.PrivateKey, error) {
	f, err := c.configScope.Open("ed25519.key")
	if err != nil {
		return nil, err
	}
	defer f.Close()

	keyRawBase64, err := io.ReadAll(f)
	if err != nil {
		return nil, err
	}

	keyRaw, err := base64.StdEncoding.DecodeString(string(keyRawBase64))
	if err != nil {
		return nil, fmt.Errorf("failed to decode key: %w", err)
	}
	if len(keyRaw) != ed25519.PrivateKeySize {
		return nil, fmt.Errorf("key file is not the correct size: %d", len(keyRaw))
	}

	return ed25519.PrivateKey(keyRaw), nil
}