package config

import (
	"errors"
	"flag"
	"fmt"
	"os"
	"strconv"
	"strings"
	"time"

	"qtransfer/internal/session"
)

type Config struct {
	Command          string
	ConfigFile       string
	SpotifyClientID  string
	SpotifyRedirect  string
	SpotifyScopes    []string
	SpotifyManual    bool
	SessionPath      string
	RememberCreds    bool
	QobuzUsername    string
	QobuzPassword    string
	QobuzAppID       string
	QobuzAppSecret   string
	QobuzSelfTest    bool
	QobuzTestWrite   bool
	QobuzTestQuery   string
	Monitor          bool
	MonitorOnce      bool
	MonitorTransfer  bool
	MonitorInterval  time.Duration
	SyncMode         string
	TargetPlaylistID int64
	LikedPlaylist    string
	DryRun           bool
	ReportPath       string
	Concurrency      int
	PlaylistNames    []string
	PlaylistURLs     []string
	AllPlaylists     bool
	IncludeLiked     bool
	NonInteractive   bool
	PublicPlaylists  bool
}

type multiFlag []string

func (m *multiFlag) String() string {
	return strings.Join(*m, ",")
}

func (m *multiFlag) Set(v string) error {
	v = strings.TrimSpace(v)
	if v == "" {
		return errors.New("playlist name cannot be empty")
	}
	*m = append(*m, v)
	return nil
}

func Load() (Config, error) {
	var cfg Config
	var playlists multiFlag
	var playlistURLs multiFlag
	command := "run"
	parseArgs := os.Args[1:]
	if len(parseArgs) > 0 {
		first := strings.ToLower(strings.TrimSpace(parseArgs[0]))
		if first == "login" || first == "logout" {
			command = "login"
			if first == "logout" {
				command = "logout"
			}
			parseArgs = parseArgs[1:]
		}
	}

	defaultScopes := "playlist-read-private,playlist-read-collaborative,user-library-read"
	defaultAppID := envOr("QOBUZ_APP_ID", "312369995")
	defaultAppSecret := envOr("QOBUZ_APP_SECRET", "e79f8b9be485692b0e5f9dd895826368")
	defaultConcurrency := envIntOr("QTRANSFER_CONCURRENCY", 4)

	flag.StringVar(&cfg.ConfigFile, "config", envOr("QTRANSFER_CONFIG", ""), "Path to TOML config file with playlist sync jobs")
	flag.StringVar(&cfg.SpotifyClientID, "spotify-client-id", envOr("SPOTIFY_CLIENT_ID", ""), "Spotify app client ID")
	flag.StringVar(&cfg.SpotifyRedirect, "spotify-redirect-uri", envOr("SPOTIFY_REDIRECT_URI", "http://127.0.0.1:8888/callback"), "Spotify OAuth redirect URI")
	scopes := flag.String("spotify-scopes", envOr("SPOTIFY_SCOPES", defaultScopes), "Comma-separated Spotify OAuth scopes")
	flag.BoolVar(&cfg.SpotifyManual, "spotify-manual-code", envBoolOr("QTRANSFER_SPOTIFY_MANUAL_CODE", true), "Enter Spotify callback code/URL manually instead of running a local callback server")
	flag.StringVar(&cfg.SessionPath, "session-file", envOr("QTRANSFER_SESSION_FILE", session.DefaultPath()), "Session file path for cached tokens/credentials")
	flag.BoolVar(&cfg.RememberCreds, "remember-creds", envBoolOr("QTRANSFER_REMEMBER_CREDS", true), "Store/reuse credentials and tokens in session file")

	flag.StringVar(&cfg.QobuzUsername, "qobuz-username", envOr("QOBUZ_USERNAME", ""), "Qobuz account username/email")
	flag.StringVar(&cfg.QobuzPassword, "qobuz-password", envOr("QOBUZ_PASSWORD", ""), "Qobuz account password")
	flag.StringVar(&cfg.QobuzAppID, "qobuz-app-id", defaultAppID, "Qobuz app ID")
	flag.StringVar(&cfg.QobuzAppSecret, "qobuz-app-secret", defaultAppSecret, "Qobuz app secret")
	flag.BoolVar(&cfg.QobuzSelfTest, "qobuz-self-test", envBoolOr("QTRANSFER_QOBUZ_SELF_TEST", false), "Run Qobuz login/verify/search checks and exit (skips Spotify)")
	flag.BoolVar(&cfg.QobuzTestWrite, "qobuz-self-test-write", envBoolOr("QTRANSFER_QOBUZ_SELF_TEST_WRITE", false), "When --qobuz-self-test is set, also create a test playlist and add one track")
	flag.StringVar(&cfg.QobuzTestQuery, "qobuz-self-test-query", envOr("QTRANSFER_QOBUZ_SELF_TEST_QUERY", "Daft Punk One More Time"), "Search query used for --qobuz-self-test")
	flag.BoolVar(&cfg.Monitor, "monitor", envBoolOr("QTRANSFER_MONITOR", false), "Monitor selected playlists for updates")
	flag.BoolVar(&cfg.MonitorOnce, "monitor-once", envBoolOr("QTRANSFER_MONITOR_ONCE", false), "Run a single monitor check then exit")
	flag.BoolVar(&cfg.MonitorTransfer, "monitor-transfer", envBoolOr("QTRANSFER_MONITOR_TRANSFER", false), "When monitoring, sync changed playlists to Qobuz")
	flag.DurationVar(&cfg.MonitorInterval, "monitor-interval", envDurationOr("QTRANSFER_MONITOR_INTERVAL", 5*time.Minute), "Monitor polling interval (e.g. 2m, 30s)")
	flag.StringVar(&cfg.SyncMode, "sync-mode", strings.ToLower(envOr("QTRANSFER_SYNC_MODE", "append")), "Sync mode for monitor-transfer: append or mirror")
	flag.Int64Var(&cfg.TargetPlaylistID, "target-playlist-id", envInt64Or("QTRANSFER_TARGET_PLAYLIST_ID", 0), "Bind source playlist (single URL only) to existing Qobuz playlist ID")

	flag.BoolVar(&cfg.DryRun, "dry-run", envBoolOr("QTRANSFER_DRY_RUN", false), "Resolve matches only, do not create or mutate Qobuz playlists")
	flag.StringVar(&cfg.ReportPath, "report", envOr("QTRANSFER_REPORT", "transfer-report.json"), "Report output path")
	flag.IntVar(&cfg.Concurrency, "concurrency", defaultConcurrency, "Concurrent track matching workers")
	flag.StringVar(&cfg.LikedPlaylist, "liked-playlist-name", envOr("QTRANSFER_LIKED_NAME", "Spotify Liked Songs"), "Name of the generated liked-songs playlist on Qobuz")

	flag.BoolVar(&cfg.AllPlaylists, "all", false, "Transfer all Spotify playlists")
	flag.BoolVar(&cfg.IncludeLiked, "liked", false, "Include Spotify liked songs")
	flag.BoolVar(&cfg.NonInteractive, "non-interactive", false, "Disable interactive playlist selection prompts")
	flag.BoolVar(&cfg.PublicPlaylists, "public-playlists", false, "Create public playlists on Qobuz (default private)")
	flag.Var(&playlists, "playlist", "Playlist name to transfer (repeatable)")
	flag.Var(&playlistURLs, "playlist-url", "Spotify playlist URL/URI/ID to transfer (repeatable)")

	if err := flag.CommandLine.Parse(preprocessArgs(parseArgs)); err != nil {
		return Config{}, err
	}
	cfg.Command = command

	cfg.PlaylistNames = playlists
	cfg.PlaylistURLs = playlistURLs
	cfg.SpotifyScopes = splitComma(*scopes)
	cfg.SyncMode = strings.ToLower(strings.TrimSpace(cfg.SyncMode))
	cfg.SyncMode = strings.ToLower(strings.TrimSpace(cfg.SyncMode))

	if err := cfg.Validate(); err != nil {
		return Config{}, err
	}

	return cfg, nil
}

func (c Config) Validate() error {
	if c.Command == "logout" {
		if strings.TrimSpace(c.SessionPath) == "" {
			return fmt.Errorf("session file path cannot be empty")
		}
		return nil
	}

	if strings.TrimSpace(c.QobuzAppID) == "" || strings.TrimSpace(c.QobuzAppSecret) == "" {
		return fmt.Errorf("qobuz app id and secret are required")
	}
	if c.QobuzSelfTest && strings.TrimSpace(c.QobuzTestQuery) == "" {
		return fmt.Errorf("qobuz self-test query cannot be empty")
	}
	if c.MonitorInterval < 2*time.Second {
		return fmt.Errorf("monitor interval must be at least 2s")
	}
	if c.SyncMode != "append" && c.SyncMode != "mirror" {
		return fmt.Errorf("invalid sync mode %q (expected append or mirror)", c.SyncMode)
	}
	if c.TargetPlaylistID < 0 {
		return fmt.Errorf("target-playlist-id must be >= 0")
	}
	if strings.TrimSpace(c.SessionPath) == "" {
		return fmt.Errorf("session file path cannot be empty")
	}
	if !c.QobuzSelfTest && c.Command != "login" {
		if strings.TrimSpace(c.SpotifyRedirect) == "" {
			return fmt.Errorf("spotify redirect URI is required")
		}
		if len(c.SpotifyScopes) == 0 {
			return fmt.Errorf("at least one Spotify scope is required")
		}
	}
	if c.Concurrency < 1 {
		return fmt.Errorf("concurrency must be >= 1")
	}
	return nil
}

func splitComma(s string) []string {
	parts := strings.Split(s, ",")
	res := make([]string, 0, len(parts))
	for _, p := range parts {
		p = strings.TrimSpace(p)
		if p != "" {
			res = append(res, p)
		}
	}
	return res
}

type boolFlag interface {
	IsBoolFlag() bool
}

// preprocessArgs moves non-flag positional arguments to the end so that flags
// can appear in any order relative to non-flag arguments.
func preprocessArgs(args []string) []string {
	boolFlagNames := make(map[string]bool)
	flag.CommandLine.VisitAll(func(f *flag.Flag) {
		if bf, ok := f.Value.(boolFlag); ok && bf.IsBoolFlag() {
			boolFlagNames[f.Name] = true
		}
	})

	var flagArgs []string
	var positional []string

	i := 0
	for i < len(args) {
		arg := args[i]
		if !strings.HasPrefix(arg, "-") {
			positional = append(positional, arg)
			i++
			continue
		}
		flagArgs = append(flagArgs, arg)
		i++
		name := strings.TrimLeft(arg, "-")
		if idx := strings.Index(name, "="); idx >= 0 {
			continue // value embedded in --flag=value form
		}
		if boolFlagNames[name] {
			continue // bool flags don't consume a following value
		}
		if i < len(args) && !strings.HasPrefix(args[i], "-") {
			flagArgs = append(flagArgs, args[i])
			i++
		}
	}
	return append(flagArgs, positional...)
}

func envOr(key, fallback string) string {
	if v := strings.TrimSpace(os.Getenv(key)); v != "" {
		return v
	}
	return fallback
}

func envIntOr(key string, fallback int) int {
	v := strings.TrimSpace(os.Getenv(key))
	if v == "" {
		return fallback
	}
	n, err := strconv.Atoi(v)
	if err != nil {
		return fallback
	}
	return n
}

func envInt64Or(key string, fallback int64) int64 {
	v := strings.TrimSpace(os.Getenv(key))
	if v == "" {
		return fallback
	}
	n, err := strconv.ParseInt(v, 10, 64)
	if err != nil {
		return fallback
	}
	return n
}

func envBoolOr(key string, fallback bool) bool {
	v := strings.TrimSpace(os.Getenv(key))
	if v == "" {
		return fallback
	}
	b, err := strconv.ParseBool(v)
	if err != nil {
		return fallback
	}
	return b
}

func envDurationOr(key string, fallback time.Duration) time.Duration {
	v := strings.TrimSpace(os.Getenv(key))
	if v == "" {
		return fallback
	}
	d, err := time.ParseDuration(v)
	if err != nil {
		return fallback
	}
	return d
}
