package dbaasui

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"

	"gopkg.in/yaml.v3"
)

const (
	Auth0Domain        = "tidb-dev.us.auth0.com"
	Auth0ClientID      = "KiwWajLJ3pto8SsZ7F35NaqNhwnoRh1c"
	uiParentZoneSuffix = ".cp9.dev.aws.tidbcloud.com"
)

// AccountAuth0Config is the Auth0 issuer/audience contract advertised by the
// Account service in one CP9 session.
type AccountAuth0Config struct {
	Domain   string `json:"domain"`
	ClientID string `json:"client_id"`
}

// Auth0ClientConfig is the read-only subset of an Auth0 application needed to
// prove that a CP9 session hostname is bounded by the shared development app.
type Auth0ClientConfig struct {
	Callbacks         []string `json:"callbacks"`
	AllowedLogoutURLs []string `json:"allowed_logout_urls"`
	WebOrigins        []string `json:"web_origins"`
}

// ParseAccountAuth0Config extracts the Account auth0 section, with the legacy
// flat shape retained only as a fallback, and ignores proxy audience values.
func ParseAccountAuth0Config(config string) (AccountAuth0Config, error) {
	type auth0Values struct {
		Domain   string `yaml:"auth0Domain"`
		ClientID string `yaml:"auth0Audience"`
	}
	var document struct {
		auth0Values `yaml:",inline"`

		Auth0 auth0Values `yaml:"auth0"`
	}
	if err := yaml.Unmarshal([]byte(config), &document); err != nil {
		return AccountAuth0Config{}, fmt.Errorf("parse session Account Auth0 config: %w", err)
	}
	values := document.Auth0
	if strings.TrimSpace(values.Domain) == "" && strings.TrimSpace(values.ClientID) == "" {
		values = document.auth0Values
	}
	result := AccountAuth0Config{
		Domain:   strings.TrimSpace(values.Domain),
		ClientID: strings.TrimSpace(values.ClientID),
	}
	if result.Domain == "" || result.ClientID == "" {
		return AccountAuth0Config{}, fmt.Errorf("session Account Auth0 config is missing auth0Domain/auth0Audience")
	}
	return result, nil
}

// ValidateAuth0Contract requires an exact one-session origin under the reviewed
// CP9 zone and the exact shared-client entries. Unrelated entries owned by other
// users of the shared dev client remain unconstrained.
func ValidateAuth0Contract(sessionURL string, account AccountAuth0Config, client Auth0ClientConfig) error {
	callback, logout, webOrigin, err := auth0URLsForSession(sessionURL)
	if err != nil {
		return err
	}
	if strings.TrimSpace(account.Domain) != Auth0Domain || strings.TrimSpace(account.ClientID) != Auth0ClientID {
		return fmt.Errorf(
			"session Account Auth0 issuer/audience drift: got domain=%q client_id=%q",
			account.Domain,
			account.ClientID,
		)
	}
	if !containsExact(client.Callbacks, callback) {
		return fmt.Errorf("missing required callback %q", callback)
	}
	if !containsExact(client.AllowedLogoutURLs, logout) {
		return fmt.Errorf("missing required logout %q", logout)
	}
	if !containsExact(client.WebOrigins, webOrigin) {
		return fmt.Errorf("missing required web origin %q", webOrigin)
	}
	return nil
}

func auth0URLsForSession(raw string) (callback, logout, webOrigin string, err error) {
	zoneName, err := validateSessionURL(raw)
	if err != nil {
		return "", "", "", err
	}
	origin := "https://*." + zoneName
	return origin + "/auth_redirect", origin + "/", origin, nil
}

func validateSessionURL(raw string) (string, error) {
	parsed, err := url.Parse(strings.TrimSpace(raw))
	if err != nil {
		return "", fmt.Errorf("session UI URL is invalid: %w", err)
	}
	hostname := strings.ToLower(strings.TrimSuffix(parsed.Hostname(), "."))
	prefix, inZone := strings.CutSuffix(hostname, uiParentZoneSuffix)
	labels := strings.Split(prefix, ".")
	if parsed.Scheme != "https" || parsed.User != nil || parsed.Port() != "" ||
		parsed.RawQuery != "" || parsed.Fragment != "" ||
		(parsed.Path != "" && parsed.Path != "/") ||
		!inZone || len(labels) != 2 || !validDNSLabel(labels[0]) || !validAWSAccountID(labels[1]) {
		return "", fmt.Errorf(
			"session UI URL %q must be https://<session>.<12-digit-account>%s",
			raw,
			uiParentZoneSuffix,
		)
	}
	return labels[1] + uiParentZoneSuffix, nil
}

func validAWSAccountID(value string) bool {
	if len(value) != 12 {
		return false
	}
	for _, char := range value {
		if char < '0' || char > '9' {
			return false
		}
	}
	return true
}

func validDNSLabel(label string) bool {
	if label == "" || len(label) > 63 || label[0] == '-' || label[len(label)-1] == '-' {
		return false
	}
	for _, char := range label {
		if (char < 'a' || char > 'z') && (char < '0' || char > '9') && char != '-' {
			return false
		}
	}
	return true
}

// FetchAuth0Client performs the only supported Auth0 operation: a scoped,
// read-only GET of the reviewed development client. It never mutates Auth0.
func FetchAuth0Client(ctx context.Context, client *http.Client, token string) (Auth0ClientConfig, error) {
	if client == nil {
		return Auth0ClientConfig{}, fmt.Errorf("Auth0 read-only HTTP client is required")
	}
	if strings.TrimSpace(token) == "" {
		return Auth0ClientConfig{}, fmt.Errorf("CP9_AUTH0_READ_TOKEN with read:clients scope is required for the read-only Auth0 check")
	}
	endpoint := "https://" + Auth0Domain + "/api/v2/clients/" + Auth0ClientID +
		"?fields=callbacks%2Callowed_logout_urls%2Cweb_origins&include_fields=true"
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, http.NoBody)
	if err != nil {
		return Auth0ClientConfig{}, fmt.Errorf("build read-only Auth0 client request: %w", err)
	}
	req.Header.Set("Authorization", "Bearer "+strings.TrimSpace(token))
	req.Header.Set("Accept", "application/json")
	resp, err := client.Do(req)
	if err != nil {
		return Auth0ClientConfig{}, fmt.Errorf("read Auth0 client contract: %w", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 1<<20))
		return Auth0ClientConfig{}, fmt.Errorf("read Auth0 client contract: unexpected HTTP status %s", resp.Status)
	}
	var result Auth0ClientConfig
	decoder := json.NewDecoder(io.LimitReader(resp.Body, 1<<20))
	if err := decoder.Decode(&result); err != nil {
		return Auth0ClientConfig{}, fmt.Errorf("decode Auth0 client contract: %w", err)
	}
	return result, nil
}

// VerifyAuth0UserLookup checks the Management API capability required by
// Account's /api/v1/auth/meta handler before a browser reaches it.
func VerifyAuth0UserLookup(ctx context.Context, client *http.Client, token, email string) error {
	if client == nil {
		return fmt.Errorf("Auth0 read-only HTTP client is required")
	}
	if strings.TrimSpace(token) == "" {
		return fmt.Errorf("CP9_AUTH0_READ_TOKEN with read:users scope is required for the Auth0 user check")
	}
	if strings.TrimSpace(email) == "" {
		return fmt.Errorf("seed identity email is required for the Auth0 user check")
	}
	endpoint := "https://" + Auth0Domain + "/api/v2/users-by-email?email=" + url.QueryEscape(strings.TrimSpace(email))
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, http.NoBody)
	if err != nil {
		return fmt.Errorf("build Auth0 user lookup request: %w", err)
	}
	req.Header.Set("Authorization", "Bearer "+strings.TrimSpace(token))
	req.Header.Set("Accept", "application/json")
	resp, err := client.Do(req)
	if err != nil {
		return fmt.Errorf("read Auth0 seed user: %w", err)
	}
	defer resp.Body.Close()
	_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 1<<20))
	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("read Auth0 seed user: read:users scope is required; unexpected HTTP status %s", resp.Status)
	}
	return nil
}

func containsExact(values []string, expected string) bool {
	for _, value := range values {
		if strings.TrimSpace(value) == expected {
			return true
		}
	}
	return false
}
