package autogencase

import (
	"context"
	"fmt"
	"log"
	"strings"
	"sync"
	"time"

	"github.com/chromedp/cdproto/network"
	"github.com/chromedp/chromedp"

	"github.com/tidbcloud/tidb-management-service/functional-test/cases/openapidedicated/autogencase/utils"
)

type TiDBCloudWebsite struct {
	LoginUrl string
}

func NewTiDBCloudWebsite(loginUrl string) TiDBCloudWebsite {
	return TiDBCloudWebsite{
		LoginUrl: loginUrl,
	}
}

func (t TiDBCloudWebsite) Token(
	ctx context.Context,
	accountEmail, accountPwd string,
) (string, time.Time, error) {
	mainAlloc, mainCancel := utils.NewHeadlessAllocator(ctx)
	// mainAlloc, mainCancel := utils.NewAllocator(ctx)
	defer mainCancel()
	browserCtx, browserCancel := context.WithCancel(mainAlloc.Context)
	defer browserCancel()

	// Listen for auth token from browser network events (preferred).
	tokenCh := make(chan string, 1)
	//nolint:contextcheck // chromedp listener must use the browser target context.
	t.installBearerTokenListener(browserCtx, tokenCh, nil)
	//nolint:contextcheck // chromedp network domain must be enabled on the browser target context.
	if err := chromedp.Run(browserCtx, network.Enable()); err != nil {
		return "", time.Time{}, err
	}

	err := t.login(mainAlloc, accountEmail, accountPwd)
	if err != nil {
		return "", time.Time{}, err
	}
	fmt.Printf("Login to %s with account %s successfully\n", t.LoginUrl, accountEmail)

	// Wait a bit for the `/me` request to be issued after login.
	token, expireTime, err := t.jwtTokenFromNetwork(ctx, tokenCh)
	if err != nil {
		fmt.Printf("Failed to get jwt token from network: %v\n", err)
		return "", time.Time{}, err
	}
	fmt.Printf("Successfully obtained token, expires at: %v\n", expireTime)
	return token, expireTime, nil
}

func (t TiDBCloudWebsite) login(
	alloc utils.Allocator,
	accountEmail, accountPwd string,
) error {
	// var payload string
	loginTasks := chromedp.Tasks{
		chromedp.Navigate(t.LoginUrl),
		// wait until the login page is loaded
		chromedp.WaitVisible(`input[type="email"]`, chromedp.ByQuery),
		chromedp.WaitVisible(`input[type="password"]`, chromedp.ByQuery),
		chromedp.WaitVisible(`button[type="submit"]`, chromedp.ByQuery),
		// input the user name and password
		chromedp.SendKeys(`input[type="email"]`, accountEmail, chromedp.ByQuery),
		chromedp.SendKeys(`input[type="password"]`, accountPwd, chromedp.ByQuery),
		// simulate clicking login
		chromedp.Click(`button[type="submit"]`, chromedp.ByQuery),
		// chromedp.Sleep(40 * time.Second),
		// chromedp.OuterHTML("html", &payload),
	}

	err := chromedp.Run(alloc.Context, loginTasks)
	// slog.Info("debug html", "payload", payload)
	// fmt.Printf("debug html: %s\n", payload)
	return err
}

func jwtExpireTime(jwtToken string) (time.Time, error) {
	jwtPayloads, err := utils.JwtPayload(jwtToken)
	if err != nil {
		return time.Time{}, fmt.Errorf("failed to parse jwt token: %w", err)
	}

	exp, ok := jwtPayloads["exp"].(float64)
	if !ok {
		return time.Time{}, fmt.Errorf("failed to get jwt token exp")
	}
	return time.Unix(int64(exp), 0), nil
}

func (t TiDBCloudWebsite) jwtTokenFromNetwork(
	ctx context.Context,
	tokenCh <-chan string,
) (string, time.Time, error) {
	waitCtx, cancel := context.WithTimeout(ctx, 60*time.Second)
	defer cancel()

	select {
	case <-waitCtx.Done():
		return "", time.Time{}, fmt.Errorf("failed to get jwt token from network: %w", waitCtx.Err())
	case token := <-tokenCh:
		if token == "" {
			return "", time.Time{}, fmt.Errorf("failed to get jwt token from network: empty token")
		}
		expireTime, err := jwtExpireTime(token)
		if err != nil {
			return "", time.Time{}, err
		}
		return token, expireTime, nil
	}
}

func (t TiDBCloudWebsite) installBearerTokenListener(
	ctx context.Context,
	tokenCh chan<- string,
	logger *log.Logger,
) {
	type reqMeta struct {
		url    string
		method string
		auth   string
	}

	var mu sync.Mutex
	reqs := map[network.RequestID]*reqMeta{}

	sendToken := func(rawAuth string, urlStr string, method string) {
		// Only accept `/me` request with GET, like python implementation.
		if !strings.EqualFold(method, "GET") {
			return
		}

		// URL may be full URL; also tolerate HTTP/2 pseudo header value like `:path`.
		isMe := strings.HasSuffix(urlStr, "/me") ||
			strings.HasSuffix(urlStr, "/me/") ||
			strings.Contains(urlStr, "/me?") ||
			strings.Contains(urlStr, "/me/?")
		if !isMe {
			return
		}

		token := extractBearerToken(rawAuth)
		if token == "" {
			return
		}

		select {
		case tokenCh <- token:
			if logger != nil {
				logger.Printf("Captured Bearer token from request: method=%s url=%s", method, urlStr)
			}
		default:
			// already captured
		}
	}

	chromedp.ListenTarget(ctx, func(ev any) {
		switch e := ev.(type) {
		case *network.EventRequestWillBeSent:
			if e.Request == nil {
				return
			}

			mu.Lock()
			meta, ok := reqs[e.RequestID]
			if !ok {
				meta = &reqMeta{}
				reqs[e.RequestID] = meta
			}
			meta.url = e.Request.URL
			meta.method = e.Request.Method
			// In some cases authorization may already exist here.
			if meta.auth == "" {
				meta.auth = headerValue(e.Request.Headers, "authorization")
			}
			urlStr := meta.url
			method := meta.method
			rawAuth := meta.auth
			mu.Unlock()

			if rawAuth != "" {
				sendToken(rawAuth, urlStr, method)
			}

		case *network.EventRequestWillBeSentExtraInfo:
			mu.Lock()
			meta, ok := reqs[e.RequestID]
			if !ok {
				meta = &reqMeta{}
				reqs[e.RequestID] = meta
			}
			if meta.auth == "" {
				meta.auth = headerValue(e.Headers, "authorization")
			}
			urlStr := meta.url
			method := meta.method
			rawAuth := meta.auth
			mu.Unlock()

			if rawAuth != "" && urlStr != "" && method != "" {
				sendToken(rawAuth, urlStr, method)
			}
		}
	})
}

func extractBearerToken(authHeader string) string {
	if authHeader == "" {
		return ""
	}
	// Typically: "Bearer <token>"
	if len(authHeader) >= 7 && strings.EqualFold(authHeader[:7], "Bearer ") {
		return strings.TrimSpace(authHeader[7:])
	}
	return ""
}

func headerValue(headers network.Headers, key string) string {
	if headers == nil {
		return ""
	}
	for k, v := range headers {
		if !strings.EqualFold(k, key) {
			continue
		}
		switch vv := v.(type) {
		case string:
			return vv
		default:
			// Best-effort fallback: headers values are usually strings.
			return fmt.Sprint(v)
		}
	}
	return ""
}
