package peer

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

	"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 (
	// EnvHost is the environment variable for the dsfx host.
	EnvHost = "DSFX_HOST"
	// EnvPort is the environment variable for the dsfx port.
	EnvPort = "DSFX_PORT"
	// EnvLogLevel is the environment variable for the dsfx log level.
	EnvLogLevel = "DSFX_LOG_LEVEL"
	// EnvDataDir is the environment variable for the dsfx storage directory.
	EnvDataDir = "DSFX_DATA_DIR"
	// EnvConfigDir is the environment variable for the dsfx configuration directory.
	EnvConfigDir = "DSFX_CONFIG_DIR"
)

const (
	// DefaultHost is the default host for the dsfxctl application.
	DefaultHost = "0.0.0.0"
	// DefaultPort is the default port for the dsfxctl application.
	DefaultPort = "8000"
	// DefaultDataDir is the default directory for the dsfx storage.
	DefaultDataDir = "/etc/dsfx/data"
	// DefaultConfigDir is the default directory for the dsfx configuration.
	DefaultConfigDir = "/etc/dsfx/config"
)

// Conf holds the configuration for the dsfxctl application.
type Conf struct {
	LogLevel string
	// Directories
	ConfigDir string
	DataDir   string
	// Networking
	Host string
	Port 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
	}
}

// Peer ...
type Peer struct {
	disk    disk.Disk
	system  system.System
	config  scoped.StorageScope
	storage scoped.StorageScope
	conf    Conf
}

// New ...
func New(disk disk.Disk, system system.System) *Peer {
	flagConfig := flag.String("configDir", "/etc/dsfx/config", "Path to the configuration directory")
	flagData := flag.String("dataDir", "/etc/dsfx/data", "Path to the data directory")
	flagLogLevel := flag.String("logLevel", "info", "The log level (debug, info, warn, error)")
	flagHost := flag.String("host", "0.0.0.0", "The host to bind to")
	flagPort := flag.String("port", "8000", "The port to bind to")

	if !flag.Parsed() {
		flag.Parse()
	}
	conf := Conf{
		LogLevel:  *flagLogLevel,
		ConfigDir: *flagConfig,
		DataDir:   *flagData,
		Host:      *flagHost,
		Port:      *flagPort,
	}

	config := scoped.New(disk, conf.ConfigDir)
	storage := scoped.New(disk, conf.DataDir)

	return &Peer{disk, system, config, storage, conf}
}

// Run ...
func (p *Peer) Run(ctx context.Context) error {
	opts := &slog.HandlerOptions{
		AddSource: false,
		Level:     p.conf.SlogLevel(),
	}
	logger := slog.New(slog.NewTextHandler(p.system.Stdout(), opts))

	slog.SetDefault(logger)
	ctx = logging.WithContext(ctx, logger)

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

	err = p.disk.MkdirAll(p.conf.DataDir, 0755)
	if err != nil {
		logger.ErrorContext(ctx, "failed to create storage dir", slog.Any("error", err))
		return err
	}

	id, err := p.loadIdentity()
	if err != nil {
		logger.ErrorContext(ctx, "failed to read key file", slog.Any("error", err))
		return err
	}

	admins, err := p.loadAdmins()
	if err != nil {
		logger.ErrorContext(ctx, "failed to read admins file", slog.Any("error", err))
		return err
	}

	if len(admins) == 0 {
		logger.WarnContext(ctx, "no admins found", slog.String("admins", "none"))
	}

	tcpAddrRaw := net.JoinHostPort(p.conf.Host, p.conf.Port)
	tcpAddr, err := net.ResolveTCPAddr("tcp", tcpAddrRaw)
	if err != nil {
		return err
	}
	addr := network.FromTCPAddr(tcpAddr, id.Public().(ed25519.PublicKey))

	// ---------------------------------------------------------------------------
	// Listener

	listener, err := network.Listen(ctx, id, addr)
	if err != nil {
		logger.ErrorContext(ctx, "listener error", slog.Any("error", err))
		return err
	}

	logger.InfoContext(ctx, "serving", slog.String("address", addr.String()))

	for {
		conn, err := listener.Accept()
		if err != nil {
			logger.ErrorContext(ctx, "accept failure", slog.Any("error", err))
			continue
		}

		go p.handleConnection(ctx, conn)
	}
}

// loadAdmins ...
func (p *Peer) loadAdmins() ([]string, error) {
	hasAdminsFile, err := p.hasAdminsFile()
	if err != nil {
		return nil, fmt.Errorf("failed to check for admins file: %w", err)
	}

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

	return p.readAdminsFile()
}

// hasAdminsFile ...
func (p *Peer) hasAdminsFile() (bool, error) {
	f, err := p.config.Open("admins")
	if errors.Is(err, os.ErrNotExist) {
		return false, nil // Key file does not exist
	}
	if err != nil {
		return false, err
	}
	defer f.Close()

	return true, nil
}

// createAdminsFile ...
func (p *Peer) createAdminsFile() error {
	f, err := p.config.Create("admins")
	if err != nil {
		return err
	}
	defer f.Close()

	return nil
}

// readAdminsFile ...
func (p *Peer) readAdminsFile() ([]string, error) {
	f, err := p.config.Open("admins")
	if err != nil {
		return nil, err
	}
	defer f.Close()

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

	lines := strings.SplitSeq(string(keyRaw), "\n")

	nonEmptyLines := []string{}
	for line := range lines {
		if strings.TrimSpace(line) != "" {
			nonEmptyLines = append(nonEmptyLines, line)
		}
	}

	return nonEmptyLines, nil
}

// loadIdentity loads the private key from the key file. If the key file does not
// exist, it creates a new key file with a generated private key and returns it.
func (p *Peer) loadIdentity() (ed25519.PrivateKey, error) {
	hasKeyFile, err := p.hasKeyFile()
	if err != nil {
		return nil, err
	}

	if hasKeyFile {
		return p.readKeyFile()
	}

	if err := p.createKeyFile(); err != nil {
		return nil, err
	}

	return p.readKeyFile()
}

// hasKeyFile checks if the key file exists in the disk storage.
func (p *Peer) hasKeyFile() (bool, error) {
	f, err := p.config.Open("ed25519.key")
	if errors.Is(err, os.ErrNotExist) {
		return false, nil // Key file does not exist
	}
	if err != nil {
		return false, err
	}
	defer f.Close()

	return true, nil
}

// createKeyFile creates a new key file with a generated private key.
func (p *Peer) createKeyFile() error {
	f, err := p.config.Create("ed25519.key")
	if err != nil {
		return err
	}
	defer f.Close()

	privkey, err := identity.Generate()
	if err != nil {
		return err
	}

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

	return nil
}

// readKeyFile reads the private key from the key file and returns it as an ed25519.PrivateKey.
func (p *Peer) readKeyFile() (ed25519.PrivateKey, error) {
	f, err := p.config.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, 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
}

func (p *Peer) handleConnection(ctx context.Context, conn net.Conn) error {
	defer conn.Close()

	logger := logging.FromContext(ctx)

	msg := make([]byte, 1024)
	n, err := conn.Read(msg)
	if err != nil {
		logger.ErrorContext(ctx, "failed to read from connection", slog.Any("error", err))
		return err
	}

	logger.InfoContext(ctx, "received msg", slog.Int("bytes", n))

	return nil
}