package main

import (
	"bytes"
	"encoding/json"
	"flag"
	"fmt"
	"os"
	"path/filepath"
)

// defaultBaseURL matches the server's default LISTEN_ADDR (:8080). The dev
// server runs on :8090; point at it with --base-url or $TINYFORGE_URL.
const defaultBaseURL = "http://localhost:8080"

// Config is the persisted CLI state at ~/.tinyforge/config.json.
type Config struct {
	BaseURL   string `json:"base_url"`
	Token     string `json:"token"`
	ExpiresAt string `json:"expires_at"`
}

// globals holds the cross-cutting flags every command accepts.
type globals struct {
	baseURL    *string
	token      *string
	configPath *string
}

// addGlobalFlags registers the shared flags on a command's flag set.
func addGlobalFlags(fs *flag.FlagSet) *globals {
	return &globals{
		baseURL:    fs.String("base-url", "", "Tinyforge server URL (default $TINYFORGE_URL or "+defaultBaseURL+")"),
		token:      fs.String("token", "", "auth token (default $TINYFORGE_TOKEN or cached config)"),
		configPath: fs.String("config", "", "config file path (default $TINYFORGE_CONFIG or ~/.tinyforge/config.json)"),
	}
}

// configFilePath resolves the config file location with precedence:
// --config flag > $TINYFORGE_CONFIG > ~/.tinyforge/config.json.
func configFilePath(g *globals) (string, error) {
	if g != nil && *g.configPath != "" {
		return *g.configPath, nil
	}
	if env := os.Getenv("TINYFORGE_CONFIG"); env != "" {
		return env, nil
	}
	home, err := os.UserHomeDir()
	if err != nil {
		return "", fmt.Errorf("locate home directory: %w", err)
	}
	return filepath.Join(home, ".tinyforge", "config.json"), nil
}

// loadConfig reads the config file. A missing file yields a zero Config and no
// error — first run is not a failure.
func loadConfig(path string) (Config, error) {
	var cfg Config
	data, err := os.ReadFile(path)
	if err != nil {
		if os.IsNotExist(err) {
			return cfg, nil
		}
		return cfg, fmt.Errorf("read config %s: %w", path, err)
	}
	// An empty or whitespace-only file (e.g. freshly touched) is treated as
	// "no config yet" rather than a parse error.
	if len(bytes.TrimSpace(data)) == 0 {
		return cfg, nil
	}
	if err := json.Unmarshal(data, &cfg); err != nil {
		return cfg, fmt.Errorf("parse config %s: %w", path, err)
	}
	return cfg, nil
}

// saveConfig writes the config file with 0600 permissions, since it holds a
// bearer token. The parent directory is created if absent.
func saveConfig(path string, cfg Config) error {
	if dir := filepath.Dir(path); dir != "" {
		if err := os.MkdirAll(dir, 0o700); err != nil {
			return fmt.Errorf("create config dir: %w", err)
		}
	}
	data, err := json.MarshalIndent(cfg, "", "  ")
	if err != nil {
		return fmt.Errorf("encode config: %w", err)
	}
	if err := os.WriteFile(path, append(data, '\n'), 0o600); err != nil {
		return fmt.Errorf("write config %s: %w", path, err)
	}
	// os.WriteFile only applies the mode when creating the file; Chmod ensures
	// 0600 even when overwriting a pre-existing, looser-permissioned config.
	if err := os.Chmod(path, 0o600); err != nil {
		return fmt.Errorf("secure config %s: %w", path, err)
	}
	return nil
}

// resolveBaseURL applies precedence: --base-url > $TINYFORGE_URL > config > default.
func resolveBaseURL(g *globals, cfg Config) string {
	if g != nil && *g.baseURL != "" {
		return *g.baseURL
	}
	if env := os.Getenv("TINYFORGE_URL"); env != "" {
		return env
	}
	if cfg.BaseURL != "" {
		return cfg.BaseURL
	}
	return defaultBaseURL
}

// resolveToken applies precedence: --token > $TINYFORGE_TOKEN > config.
func resolveToken(g *globals, cfg Config) string {
	if g != nil && *g.token != "" {
		return *g.token
	}
	if env := os.Getenv("TINYFORGE_TOKEN"); env != "" {
		return env
	}
	return cfg.Token
}

// session bundles the resolved client with the loaded config and its path, so
// commands can both make requests and persist updates (e.g. login).
type session struct {
	client     *Client
	cfg        Config
	configPath string
}

// newSession loads config and builds a client with resolved base URL + token.
func newSession(g *globals) (*session, error) {
	path, err := configFilePath(g)
	if err != nil {
		return nil, err
	}
	cfg, err := loadConfig(path)
	if err != nil {
		return nil, err
	}
	return &session{
		client:     newClient(resolveBaseURL(g, cfg), resolveToken(g, cfg)),
		cfg:        cfg,
		configPath: path,
	}, nil
}
