package spotify

import (
	"bufio"
	"context"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"os"
	"os/exec"
	"runtime"
	"strings"
	"time"
)

type AuthConfig struct {
	ClientID    string
	RedirectURI string
	Scopes      []string
	ManualCode  bool
}

type Token struct {
	AccessToken  string `json:"access_token"`
	TokenType    string `json:"token_type"`
	Scope        string `json:"scope"`
	ExpiresIn    int    `json:"expires_in"`
	RefreshToken string `json:"refresh_token"`
}

func LoginWithPKCE(ctx context.Context, cfg AuthConfig) (Token, error) {
	redirectURL, err := url.Parse(cfg.RedirectURI)
	if err != nil {
		return Token{}, fmt.Errorf("invalid redirect URI: %w", err)
	}

	codeVerifier, err := randomURLSafe(64)
	if err != nil {
		return Token{}, err
	}
	state, err := randomURLSafe(24)
	if err != nil {
		return Token{}, err
	}

	h := sha256.Sum256([]byte(codeVerifier))
	codeChallenge := base64.RawURLEncoding.EncodeToString(h[:])

	authURL := "https://accounts.spotify.com/authorize?" + url.Values{
		"response_type":         []string{"code"},
		"client_id":             []string{cfg.ClientID},
		"scope":                 []string{strings.Join(cfg.Scopes, " ")},
		"redirect_uri":          []string{cfg.RedirectURI},
		"state":                 []string{state},
		"code_challenge_method": []string{"S256"},
		"code_challenge":        []string{codeChallenge},
	}.Encode()

	if cfg.ManualCode {
		return loginManual(ctx, cfg, authURL, state, codeVerifier)
	}

	tok, err := loginWithCallbackServer(ctx, cfg, redirectURL, authURL, state, codeVerifier)
	if err == nil {
		return tok, nil
	}

	if strings.Contains(strings.ToLower(err.Error()), "listen callback server") {
		fmt.Println("Local callback server unavailable; falling back to manual code entry.")
		return loginManual(ctx, cfg, authURL, state, codeVerifier)
	}

	return Token{}, err
}

func loginWithCallbackServer(ctx context.Context, cfg AuthConfig, redirectURL *url.URL, authURL, state, codeVerifier string) (Token, error) {

	codeCh := make(chan string, 1)
	errCh := make(chan error, 1)

	mux := http.NewServeMux()
	server := &http.Server{
		Addr:    redirectURL.Host,
		Handler: mux,
	}

	mux.HandleFunc(redirectURL.Path, func(w http.ResponseWriter, r *http.Request) {
		q := r.URL.Query()
		if q.Get("state") != state {
			http.Error(w, "Invalid state", http.StatusBadRequest)
			select {
			case errCh <- fmt.Errorf("state mismatch in spotify callback"):
			default:
			}
			return
		}
		if e := q.Get("error"); e != "" {
			http.Error(w, "Spotify authorization failed", http.StatusBadRequest)
			select {
			case errCh <- fmt.Errorf("spotify auth error: %s", e):
			default:
			}
			return
		}
		code := q.Get("code")
		if code == "" {
			http.Error(w, "Missing authorization code", http.StatusBadRequest)
			select {
			case errCh <- fmt.Errorf("spotify callback missing code"):
			default:
			}
			return
		}

		_, _ = io.WriteString(w, "Spotify authorization complete. You can close this tab.")
		select {
		case codeCh <- code:
		default:
		}
	})

	ln, err := net.Listen("tcp", redirectURL.Host)
	if err != nil {
		return Token{}, fmt.Errorf("listen callback server: %w", err)
	}

	go func() {
		if serveErr := server.Serve(ln); serveErr != nil && serveErr != http.ErrServerClosed {
			select {
			case errCh <- serveErr:
			default:
			}
		}
	}()

	_ = openBrowser(authURL)
	fmt.Printf("Open this URL in your browser if it did not open automatically:\n%s\n\n", authURL)

	var code string
	select {
	case <-ctx.Done():
		_ = server.Shutdown(context.Background())
		return Token{}, ctx.Err()
	case e := <-errCh:
		_ = server.Shutdown(context.Background())
		return Token{}, e
	case code = <-codeCh:
	}

	shutdownCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()
	_ = server.Shutdown(shutdownCtx)

	return exchangeCode(ctx, cfg, code, codeVerifier)
}

func loginManual(ctx context.Context, cfg AuthConfig, authURL, expectedState, codeVerifier string) (Token, error) {
	_ = openBrowser(authURL)
	fmt.Printf("Open this URL in your browser if it did not open automatically:\n%s\n\n", authURL)
	fmt.Printf("After Spotify redirects to %s, copy the full URL from your browser and paste it here.\n", cfg.RedirectURI)
	fmt.Println("If your browser only shows a code, you can paste that code directly.")

	scanner := bufio.NewScanner(os.Stdin)
	for {
		fmt.Print("Paste callback URL/code: ")
		if !scanner.Scan() {
			if scanner.Err() != nil {
				return Token{}, scanner.Err()
			}
			return Token{}, fmt.Errorf("stdin closed before spotify auth code was provided")
		}

		select {
		case <-ctx.Done():
			return Token{}, ctx.Err()
		default:
		}

		input := strings.TrimSpace(scanner.Text())
		if input == "" {
			continue
		}

		code, err := extractAuthCode(input, expectedState)
		if err != nil {
			fmt.Printf("Could not parse code: %v\n", err)
			continue
		}

		return exchangeCode(ctx, cfg, code, codeVerifier)
	}
}

func extractAuthCode(input, expectedState string) (string, error) {
	input = strings.TrimSpace(input)

	if !strings.Contains(input, "code=") && !strings.Contains(input, "://") {
		return input, nil
	}

	queryString := ""
	if strings.Contains(input, "://") {
		u, err := url.Parse(input)
		if err != nil {
			return "", fmt.Errorf("invalid callback URL")
		}
		queryString = u.RawQuery
	} else {
		queryString = strings.TrimPrefix(input, "?")
	}

	q, err := url.ParseQuery(queryString)
	if err != nil {
		return "", fmt.Errorf("invalid callback query")
	}

	if e := q.Get("error"); e != "" {
		return "", fmt.Errorf("spotify returned error: %s", e)
	}

	if gotState := q.Get("state"); expectedState != "" && gotState != "" && gotState != expectedState {
		return "", fmt.Errorf("state mismatch")
	}

	code := strings.TrimSpace(q.Get("code"))
	if code == "" {
		return "", fmt.Errorf("missing code parameter")
	}
	return code, nil
}

func exchangeCode(ctx context.Context, cfg AuthConfig, code, codeVerifier string) (Token, error) {
	body := url.Values{
		"client_id":     []string{cfg.ClientID},
		"grant_type":    []string{"authorization_code"},
		"code":          []string{code},
		"redirect_uri":  []string{cfg.RedirectURI},
		"code_verifier": []string{codeVerifier},
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://accounts.spotify.com/api/token", strings.NewReader(body.Encode()))
	if err != nil {
		return Token{}, err
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return Token{}, err
	}
	defer resp.Body.Close()

	if resp.StatusCode >= 300 {
		b, _ := io.ReadAll(resp.Body)
		return Token{}, fmt.Errorf("spotify token exchange failed (%d): %s", resp.StatusCode, strings.TrimSpace(string(b)))
	}

	var tok Token
	if err := json.NewDecoder(resp.Body).Decode(&tok); err != nil {
		return Token{}, err
	}
	if tok.AccessToken == "" {
		return Token{}, fmt.Errorf("spotify token response missing access_token")
	}
	return tok, nil
}

func RefreshAccessToken(ctx context.Context, clientID, refreshToken string) (Token, error) {
	body := url.Values{
		"client_id":     []string{clientID},
		"grant_type":    []string{"refresh_token"},
		"refresh_token": []string{refreshToken},
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://accounts.spotify.com/api/token", strings.NewReader(body.Encode()))
	if err != nil {
		return Token{}, err
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return Token{}, err
	}
	defer resp.Body.Close()

	if resp.StatusCode >= 300 {
		b, _ := io.ReadAll(resp.Body)
		return Token{}, fmt.Errorf("spotify refresh failed (%d): %s", resp.StatusCode, strings.TrimSpace(string(b)))
	}

	var tok Token
	if err := json.NewDecoder(resp.Body).Decode(&tok); err != nil {
		return Token{}, err
	}
	if tok.AccessToken == "" {
		return Token{}, fmt.Errorf("spotify refresh response missing access_token")
	}
	if tok.RefreshToken == "" {
		tok.RefreshToken = refreshToken
	}
	return tok, nil
}

func randomURLSafe(n int) (string, error) {
	b := make([]byte, n)
	if _, err := rand.Read(b); err != nil {
		return "", err
	}
	return base64.RawURLEncoding.EncodeToString(b), nil
}

func openBrowser(u string) error {
	var cmd *exec.Cmd
	switch runtime.GOOS {
	case "linux":
		cmd = exec.Command("xdg-open", u)
	case "darwin":
		cmd = exec.Command("open", u)
	case "windows":
		cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", u)
	default:
		return fmt.Errorf("unsupported OS for auto-open")
	}
	return cmd.Start()
}
