// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
//
// SPDX-License-Identifier: LicenseRef-MutuaL-1.2

// Package appconfig loads cooked-mcp configuration from TOML and environment variables.
package appconfig

import (
	"context"
	"errors"
	"fmt"
	"net"
	"net/url"
	"os"
	"path/filepath"

	"github.com/BurntSushi/toml"

	"git.secluded.site/cooked-mcp/internal/configvalue"
)

const (
	defaultBaseURL  = "https://cooked.wiki"
	defaultHTTPAddr = "127.0.0.1:8123"
)

var userHomeDir = os.UserHomeDir

// FileConfig is the TOML configuration shape.
type FileConfig struct {
	User   UserConfig   `toml:"user"`
	Cooked CookedConfig `toml:"cooked"`
	MCP    MCPConfig    `toml:"mcp"`
}

// UserConfig contains Cooked login credentials.
type UserConfig struct {
	Name     string `toml:"name"`
	Password string `toml:"password"`
}

// CookedConfig contains Cooked API configuration.
type CookedConfig struct {
	BaseURL string `toml:"base_url"`
}

// MCPConfig contains MCP transport configuration.
type MCPConfig struct {
	HTTPAddr  string  `toml:"http_addr"`
	HTTPToken *string `toml:"http_token"`
}

// Config is fully resolved runtime configuration.
type Config struct {
	Username  string
	Password  string
	BaseURL   *url.URL
	HTTPAddr  string
	HTTPToken string
}

// Load reads and resolves configuration. Missing default config files are ignored.
func Load(ctx context.Context, path string, resolver *configvalue.Resolver) (Config, error) {
	fileConfig, err := loadFile(path)
	if err != nil {
		return Config{}, err
	}

	username, err := requiredConfigValue(
		ctx,
		resolver,
		"COOKED_LOGIN_USERNAME",
		fileConfig.User.Name,
		"Cooked username",
	)
	if err != nil {
		return Config{}, err
	}

	password, err := requiredConfigValue(
		ctx,
		resolver,
		"COOKED_LOGIN_PASSWORD",
		fileConfig.User.Password,
		"Cooked password",
	)
	if err != nil {
		return Config{}, err
	}

	baseURL, err := baseURL(ctx, resolver, fileConfig.Cooked.BaseURL)
	if err != nil {
		return Config{}, err
	}
	httpAddr := defaultHTTPAddr
	if fileConfig.MCP.HTTPAddr != "" {
		httpAddr = fileConfig.MCP.HTTPAddr
	}
	if envHTTPAddr := os.Getenv("COOKED_MCP_HTTP_ADDR"); envHTTPAddr != "" {
		httpAddr = envHTTPAddr
	}
	httpToken, err := httpToken(ctx, resolver, fileConfig.MCP.HTTPToken)
	if err != nil {
		return Config{}, err
	}

	return Config{
		Username:  username,
		Password:  password,
		BaseURL:   baseURL,
		HTTPAddr:  httpAddr,
		HTTPToken: httpToken,
	}, nil
}

// DefaultPath returns the default TOML configuration path.
func DefaultPath() (string, error) {
	if configHome := os.Getenv("XDG_CONFIG_HOME"); configHome != "" {
		return filepath.Join(configHome, "cooked-mcp", "config.toml"), nil
	}

	home, err := userHomeDir()
	if err != nil {
		return "", fmt.Errorf("resolve default config path: %w", err)
	}
	if home == "" {
		return "", fmt.Errorf("resolve default config path: home directory is empty")
	}

	return filepath.Join(home, ".config", "cooked-mcp", "config.toml"), nil
}

func loadFile(path string) (FileConfig, error) {
	explicitPath := path != ""
	if path == "" {
		defaultPath, err := DefaultPath()
		if err != nil {
			return FileConfig{}, err
		}
		path = defaultPath
	}

	var fileConfig FileConfig
	if _, err := os.Stat(path); err != nil {
		if errors.Is(err, os.ErrNotExist) && !explicitPath {
			return fileConfig, nil
		}
		if errors.Is(err, os.ErrNotExist) {
			return FileConfig{}, fmt.Errorf("config file %q does not exist", path)
		}
		return FileConfig{}, fmt.Errorf("stat config file: %w", err)
	}

	if _, err := toml.DecodeFile(path, &fileConfig); err != nil {
		return FileConfig{}, fmt.Errorf("decode config file: %w", err)
	}

	return fileConfig, nil
}

func requiredConfigValue(
	ctx context.Context,
	resolver *configvalue.Resolver,
	envName, config, description string,
) (string, error) {
	if os.Getenv(envName) != "" || config != "" {
		return configvalue.ResolveWithExplicitEnvOverride(ctx, resolver, envName, config, description)
	}

	return "", fmt.Errorf("missing %s configuration", description)
}

func baseURL(ctx context.Context, resolver *configvalue.Resolver, config string) (*url.URL, error) {
	envBaseURL := os.Getenv("COOKED_BASE_URL")
	baseURLString := envBaseURL
	if envBaseURL == "" {
		baseURLString = defaultBaseURL
	}
	if envBaseURL == "" && config != "" {
		resolved, err := resolver.ResolveOrThrow(ctx, config, "Cooked base URL")
		if err != nil {
			return nil, err
		}
		baseURLString = resolved
	}

	parsed, err := url.Parse(baseURLString)
	if err != nil {
		return nil, fmt.Errorf("parse Cooked base URL: %w", err)
	}

	if parsed.Scheme != "http" && parsed.Scheme != "https" {
		return nil, fmt.Errorf("cooked base URL must be an HTTP URL")
	}
	if parsed.Host == "" {
		return nil, fmt.Errorf("cooked base URL must include a host")
	}
	if parsed.Scheme == "http" && !isLoopbackHost(parsed.Hostname()) {
		return nil, fmt.Errorf("cooked base URL must use https unless it is localhost")
	}

	return parsed, nil
}

func httpToken(ctx context.Context, resolver *configvalue.Resolver, config *string) (string, error) {
	if value, ok := os.LookupEnv("COOKED_MCP_HTTP_TOKEN"); ok {
		if value == "" {
			return "", fmt.Errorf("MCP HTTP token must not be empty")
		}

		return value, nil
	}
	if config == nil {
		return "", nil
	}

	value, err := resolver.ResolveOrThrow(ctx, *config, "MCP HTTP token")
	if err != nil {
		return "", err
	}
	if value == "" {
		return "", fmt.Errorf("MCP HTTP token must not be empty")
	}

	return value, nil
}

func isLoopbackHost(host string) bool {
	if host == "localhost" {
		return true
	}

	parsed := net.ParseIP(host)
	return parsed != nil && parsed.IsLoopback()
}
