package dbaasui

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"time"
)

const (
	BrowserAcceptanceAPIVersion = "cp9.tidbcloud.com/v1alpha1"
	BrowserAcceptanceKind       = "DbaasUIBrowserAcceptance"

	BrowserOutcomePassed = "passed"
	BrowserOutcomeFailed = "failed"

	BrowserStageAuthenticatedHistory = "authenticated-history"
	BrowserStageAuthCallback         = "auth-callback"
	BrowserStageAuthenticatedShell   = "authenticated-shell"
	BrowserStageHistoryRefresh       = "history-refresh"
)

// BrowserAcceptanceEvidence is the redacted operator result of one real Auth0
// callback and authenticated-browser attempt. It intentionally has no
// free-form field that could retain credentials or token-bearing traces.
type BrowserAcceptanceEvidence struct {
	APIVersion string    `json:"api_version"`
	Kind       string    `json:"kind"`
	Session    string    `json:"session"`
	Origin     string    `json:"origin"`
	Outcome    string    `json:"outcome"`
	Stage      string    `json:"stage"`
	Path       string    `json:"path"`
	CheckedAt  time.Time `json:"checked_at"`
}

func BrowserAcceptancePath(stateDir string) string {
	return filepath.Join(stateDir, "tilt", "reports", "browser-acceptance.json")
}

func NewBrowserAcceptanceEvidence(
	session,
	origin,
	outcome,
	stage,
	requestPath string,
	checkedAt time.Time,
) (*BrowserAcceptanceEvidence, error) {
	evidence := &BrowserAcceptanceEvidence{
		APIVersion: BrowserAcceptanceAPIVersion,
		Kind:       BrowserAcceptanceKind,
		Session:    strings.TrimSpace(session),
		Origin:     strings.TrimSpace(origin),
		Outcome:    strings.TrimSpace(outcome),
		Stage:      strings.TrimSpace(stage),
		Path:       strings.TrimSpace(requestPath),
		CheckedAt:  checkedAt.UTC(),
	}
	if err := evidence.Validate(); err != nil {
		return nil, err
	}
	return evidence, nil
}

func (e *BrowserAcceptanceEvidence) Validate() error {
	if e == nil {
		return fmt.Errorf("browser acceptance evidence is nil")
	}
	if e.APIVersion != BrowserAcceptanceAPIVersion || e.Kind != BrowserAcceptanceKind {
		return fmt.Errorf("unsupported browser acceptance evidence contract")
	}
	if e.Session == "" {
		return fmt.Errorf("browser acceptance session is required")
	}
	origin, err := url.Parse(e.Origin)
	if err != nil || origin.Scheme != "https" || origin.Host == "" ||
		origin.User != nil || origin.RawQuery != "" || origin.Fragment != "" ||
		(origin.Path != "" && origin.Path != "/") {
		return fmt.Errorf("browser acceptance origin must be an HTTPS origin")
	}
	e.Origin = strings.TrimSuffix(origin.String(), "/")
	switch e.Outcome {
	case BrowserOutcomePassed:
		if e.Stage != BrowserStageAuthenticatedHistory {
			return fmt.Errorf("passed browser acceptance requires stage %q", BrowserStageAuthenticatedHistory)
		}
	case BrowserOutcomeFailed:
		switch e.Stage {
		case BrowserStageAuthCallback, BrowserStageAuthenticatedShell, BrowserStageHistoryRefresh:
		default:
			return fmt.Errorf("failed browser acceptance has unsupported stage %q", e.Stage)
		}
	default:
		return fmt.Errorf("browser acceptance outcome must be passed or failed")
	}
	requestPath, err := url.ParseRequestURI(e.Path)
	if err != nil || requestPath.IsAbs() || requestPath.Host != "" || !strings.HasPrefix(requestPath.Path, "/") {
		return fmt.Errorf("browser acceptance path must be a same-origin absolute path")
	}
	e.Path = requestPath.Path
	if e.CheckedAt.IsZero() {
		return fmt.Errorf("browser acceptance checked_at is required")
	}
	return nil
}

func (e *BrowserAcceptanceEvidence) Message() string {
	if e == nil {
		return ""
	}
	if e.Outcome == BrowserOutcomePassed {
		return "real Auth0 callback, authenticated shell, and direct history-path refresh passed"
	}
	return "real browser acceptance failed at " + e.Stage
}

func LoadBrowserAcceptance(stateDir string) (*BrowserAcceptanceEvidence, error) {
	data, err := os.ReadFile(BrowserAcceptancePath(stateDir))
	if err != nil {
		return nil, err
	}
	var evidence BrowserAcceptanceEvidence
	if err := json.Unmarshal(data, &evidence); err != nil {
		return nil, fmt.Errorf("decode browser acceptance evidence: %w", err)
	}
	if err := evidence.Validate(); err != nil {
		return nil, err
	}
	return &evidence, nil
}

func SaveBrowserAcceptance(stateDir string, evidence *BrowserAcceptanceEvidence) error {
	if err := evidence.Validate(); err != nil {
		return err
	}
	data, err := json.MarshalIndent(evidence, "", "  ")
	if err != nil {
		return fmt.Errorf("marshal browser acceptance evidence: %w", err)
	}
	data = append(data, '\n')
	path := BrowserAcceptancePath(stateDir)
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		return fmt.Errorf("create browser acceptance evidence directory: %w", err)
	}
	temporary, err := os.CreateTemp(filepath.Dir(path), ".browser-acceptance-*.tmp")
	if err != nil {
		return fmt.Errorf("create browser acceptance temporary file: %w", err)
	}
	temporaryPath := temporary.Name()
	defer os.Remove(temporaryPath)
	if err := temporary.Chmod(0o600); err != nil {
		_ = temporary.Close()
		return fmt.Errorf("set browser acceptance temporary file mode: %w", err)
	}
	if _, err := temporary.Write(data); err != nil {
		_ = temporary.Close()
		return fmt.Errorf("write browser acceptance temporary file: %w", err)
	}
	if err := temporary.Close(); err != nil {
		return fmt.Errorf("close browser acceptance temporary file: %w", err)
	}
	if err := os.Rename(temporaryPath, path); err != nil {
		return fmt.Errorf("replace browser acceptance evidence: %w", err)
	}
	return nil
}

func IsBrowserAcceptanceMissing(err error) bool {
	return errors.Is(err, os.ErrNotExist)
}
