// Package auth implements the interactive OAuth 2.0 authorization-code flow used
// by `tapir auth` to mint a YouTube refresh token for the single Stage-0 user.
// It is written fresh on golang.org/x/oauth2 (ADR-006): the ingestion repo's
// oauth package is inbound MCP server auth and unrelated to this outbound
// provider flow.
//
// The minted refresh token is persisted through the SecretStore port (the file
// store at Stage 0) and is never logged or returned to the caller. Only the
// opaque token ref crosses package boundaries afterward.
package auth

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"

	"golang.org/x/oauth2"
)

// GoogleEndpoint is Google's OAuth2 endpoint, inlined to avoid the heavy
// golang.org/x/oauth2/google dependency for two URLs (mirrors the youtube
// adapter's choice).
var GoogleEndpoint = oauth2.Endpoint{
	AuthURL:  "https://accounts.google.com/o/oauth2/auth",
	TokenURL: "https://oauth2.googleapis.com/token",
}

// DefaultScopes request read access to subscriptions/search and the force-ssl
// scope the Data API captions endpoints require.
var DefaultScopes = []string{
	"https://www.googleapis.com/auth/youtube.readonly",
	"https://www.googleapis.com/auth/youtube.force-ssl",
}

// TokenWriter persists a secret value under an opaque ref. *secrets.FileStore
// satisfies it; tests use a fake. (The read side is ports.SecretStore.)
type TokenWriter interface {
	Put(ref, value string) error
}

// Config wires the flow. Endpoint defaults to GoogleEndpoint when zero, so tests
// can point it at an httptest token server.
type Config struct {
	ClientID     string
	ClientSecret string
	RedirectURL  string // e.g. "http://localhost:8080/callback"
	Scopes       []string
	TokenRef     string // SecretStore ref to persist the refresh token under
	Endpoint     oauth2.Endpoint
}

func oauthConfig(c Config) *oauth2.Config {
	ep := c.Endpoint
	if ep == (oauth2.Endpoint{}) {
		ep = GoogleEndpoint
	}
	scopes := c.Scopes
	if len(scopes) == 0 {
		scopes = DefaultScopes
	}
	return &oauth2.Config{
		ClientID:     c.ClientID,
		ClientSecret: c.ClientSecret,
		RedirectURL:  c.RedirectURL,
		Scopes:       scopes,
		Endpoint:     ep,
	}
}

// AuthCodeURL builds the provider consent URL the web connect flow redirects to
// (internal/web). It reuses oauthConfig and pins access_type=offline + prompt=
// consent so Google returns a refresh token even on a repeat authorization —
// without one, Exchange would reject the result. state is the per-request CSRF
// token the caller binds to the user and verifies on the callback.
func AuthCodeURL(c Config, state string) string {
	return oauthConfig(c).AuthCodeURL(state,
		oauth2.AccessTypeOffline,
		oauth2.SetAuthURLParam("prompt", "consent"))
}

// Exchange swaps an authorization code for a token and persists the refresh
// token through the writer. It errors if the provider returned no refresh token
// (e.g. consent was not forced with offline access), since without one the
// token is useless for the unattended run loop. The token is never logged.
func Exchange(ctx context.Context, c Config, secrets TokenWriter, code string) error {
	conf := oauthConfig(c)
	tok, err := conf.Exchange(ctx, code)
	if err != nil {
		return fmt.Errorf("auth: exchange code: %w", err)
	}
	if tok.RefreshToken == "" {
		return fmt.Errorf("auth: provider returned no refresh token (re-consent with offline access)")
	}
	if err := secrets.Put(c.TokenRef, tok.RefreshToken); err != nil {
		return fmt.Errorf("auth: persist refresh token: %w", err)
	}
	return nil
}

// Run performs the full interactive flow: it binds a local listener on the
// redirect URL's host, prints the consent URL to out, waits for the provider's
// redirect (validating the state parameter), then exchanges the captured code
// and persists the refresh token. It blocks until the callback arrives, ctx is
// cancelled, or the listener fails.
func Run(ctx context.Context, c Config, secrets TokenWriter, out io.Writer) error {
	conf := oauthConfig(c)

	u, err := url.Parse(c.RedirectURL)
	if err != nil {
		return fmt.Errorf("auth: parse redirect url %q: %w", c.RedirectURL, err)
	}
	ln, err := net.Listen("tcp", u.Host)
	if err != nil {
		return fmt.Errorf("auth: bind redirect listener on %q: %w", u.Host, err)
	}

	state, err := randomState()
	if err != nil {
		return err
	}

	type result struct {
		code string
		err  error
	}
	resCh := make(chan result, 1)

	mux := http.NewServeMux()
	mux.HandleFunc(u.Path, func(w http.ResponseWriter, r *http.Request) {
		q := r.URL.Query()
		if e := q.Get("error"); e != "" {
			http.Error(w, "authorization failed: "+e, http.StatusBadRequest)
			resCh <- result{err: fmt.Errorf("auth: provider returned error %q", e)}
			return
		}
		if q.Get("state") != state {
			http.Error(w, "state mismatch", http.StatusBadRequest)
			resCh <- result{err: fmt.Errorf("auth: state mismatch (possible CSRF)")}
			return
		}
		code := q.Get("code")
		if code == "" {
			http.Error(w, "missing code", http.StatusBadRequest)
			resCh <- result{err: fmt.Errorf("auth: redirect carried no code")}
			return
		}
		_, _ = io.WriteString(w, "Tapir: authorization received. You can close this tab.")
		resCh <- result{code: code}
	})

	srv := &http.Server{Handler: mux}
	go func() { _ = srv.Serve(ln) }()
	defer func() { _ = srv.Shutdown(context.Background()) }()

	// AccessTypeOffline + ApprovalForce make Google return a refresh token even
	// on re-authorization; without them a repeat consent yields only an access
	// token and Exchange would reject it.
	authURL := conf.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.ApprovalForce)
	_, _ = fmt.Fprintf(out, "Open this URL to authorize Tapir, then return here:\n\n%s\n\n", authURL)

	select {
	case <-ctx.Done():
		return ctx.Err()
	case res := <-resCh:
		if res.err != nil {
			return res.err
		}
		if err := Exchange(ctx, c, secrets, res.code); err != nil {
			return err
		}
		_, _ = fmt.Fprintln(out, "Refresh token stored. You can now run `tapir run`.")
		return nil
	}
}

func randomState() (string, error) {
	b := make([]byte, 16)
	if _, err := rand.Read(b); err != nil {
		return "", fmt.Errorf("auth: generate state: %w", err)
	}
	return hex.EncodeToString(b), nil
}
