package tokenanalysis

import (
	"context"
	"crypto/rand"
	"crypto/subtle"
	"embed"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"net"
	"net/http"
	"net/url"
	"strings"
	"time"
)

//go:embed ui/*
var uiFiles embed.FS

type UIOptions struct {
	Client  Client
	NoOpen  bool
	OpenURL func(string) error
	Out     io.Writer
}

func RunUI(ctx context.Context, options UIOptions) error {
	token, err := newUIAccessToken()
	if err != nil {
		return err
	}
	listener, err := net.Listen("tcp4", "127.0.0.1:0")
	if err != nil {
		return fmt.Errorf("listen on local AgentTeam Token address: %w", err)
	}
	defer listener.Close()
	origin := "http://" + listener.Addr().String()
	assets, err := fs.Sub(uiFiles, "ui")
	if err != nil {
		return err
	}
	server := &http.Server{
		Handler:           &uiServer{token: token, origin: origin, client: options.Client, assets: assets},
		ReadHeaderTimeout: 10 * time.Second,
		ReadTimeout:       10 * time.Second,
		WriteTimeout:      30 * time.Second,
		IdleTimeout:       30 * time.Second,
		MaxHeaderBytes:    16 << 10,
	}
	accessURL := origin + "/#" + url.PathEscape(token)
	if options.Out != nil {
		fmt.Fprintf(options.Out, "AgentTeam Token UI: %s\n", accessURL)
		fmt.Fprintln(options.Out, "仅监听本机，不上传数据。按 Ctrl+C 停止。")
	}
	if !options.NoOpen && options.OpenURL != nil {
		_ = options.OpenURL(accessURL)
	}
	errCh := make(chan error, 1)
	go func() { errCh <- server.Serve(listener) }()
	select {
	case <-ctx.Done():
		shutdown, cancel := context.WithTimeout(context.Background(), 3*time.Second)
		defer cancel()
		_ = server.Shutdown(shutdown)
		err = <-errCh
	case err = <-errCh:
	}
	if err != nil && !errors.Is(err, http.ErrServerClosed) {
		return err
	}
	return nil
}

type uiServer struct {
	token  string
	origin string
	client Client
	assets fs.FS
}

func (server *uiServer) ServeHTTP(response http.ResponseWriter, request *http.Request) {
	setUISecurityHeaders(response)
	if !loopbackRequest(request) || !server.validOrigin(request) {
		http.Error(response, "forbidden", http.StatusForbidden)
		return
	}
	switch request.URL.Path {
	case "/", "/index.html":
		server.serveAsset(response, request, "index.html", "text/html; charset=utf-8")
	case "/app.js":
		server.serveAsset(response, request, "app.js", "text/javascript; charset=utf-8")
	case "/styles.css":
		server.serveAsset(response, request, "styles.css", "text/css; charset=utf-8")
	case "/api/analysis":
		server.requireToken(response, request, server.handleAnalysis)
	case "/api/collection":
		server.requireToken(response, request, server.handleCollection)
	case "/api/compare":
		server.requireToken(response, request, server.handleComparison)
	default:
		http.NotFound(response, request)
	}
}

func (server *uiServer) handleAnalysis(response http.ResponseWriter, request *http.Request) {
	if request.Method != http.MethodGet {
		response.WriteHeader(http.StatusMethodNotAllowed)
		return
	}
	report, err := server.client.Analyze(request.Context(), Request{
		Window:   request.URL.Query().Get("window"),
		AgentRef: request.URL.Query().Get("agentRef"),
		RunRef:   request.URL.Query().Get("runRef"),
	}, AnalysisModePassive)
	if err != nil {
		writeUIError(response, err, http.StatusBadGateway)
		return
	}
	if request.URL.Query().Get("portable") == "true" {
		report = Portable(report)
	}
	writeUIJSON(response, report)
}

func (server *uiServer) handleCollection(response http.ResponseWriter, request *http.Request) {
	var (
		status CollectionStatus
		err    error
	)
	switch request.Method {
	case http.MethodGet:
		status, err = server.client.collection(request.Context(), http.MethodGet, nil)
	case http.MethodPut:
		reader := http.MaxBytesReader(response, request.Body, 64<<10)
		defer reader.Close()
		var input struct {
			Enabled bool `json:"enabled"`
		}
		decoder := json.NewDecoder(reader)
		decoder.DisallowUnknownFields()
		if err := decoder.Decode(&input); err != nil {
			writeUIError(response, errors.New("invalid request"), http.StatusBadRequest)
			return
		}
		var extra any
		if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) {
			writeUIError(response, errors.New("request must contain one JSON value"), http.StatusBadRequest)
			return
		}
		status, err = server.client.SetCollection(request.Context(), input.Enabled)
	default:
		response.WriteHeader(http.StatusMethodNotAllowed)
		return
	}
	if err != nil {
		writeUIError(response, err, http.StatusBadGateway)
		return
	}
	writeUIJSON(response, status)
}

func (server *uiServer) handleComparison(response http.ResponseWriter, request *http.Request) {
	if request.Method != http.MethodPost {
		response.WriteHeader(http.StatusMethodNotAllowed)
		return
	}
	reader := http.MaxBytesReader(response, request.Body, 2*MaxArtifactBytes+64<<10)
	defer reader.Close()
	var input struct {
		Baseline  Report `json:"baseline"`
		Candidate Report `json:"candidate"`
	}
	decoder := json.NewDecoder(reader)
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(&input); err != nil {
		writeUIError(response, errors.New("invalid reports"), http.StatusBadRequest)
		return
	}
	var extra any
	if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) {
		writeUIError(response, errors.New("request must contain one JSON value"), http.StatusBadRequest)
		return
	}
	if err := ValidatePortableReport(input.Baseline); err != nil {
		writeUIError(response, err, http.StatusBadRequest)
		return
	}
	if err := ValidatePortableReport(input.Candidate); err != nil {
		writeUIError(response, err, http.StatusBadRequest)
		return
	}
	writeUIJSON(response, Compare(input.Baseline, input.Candidate))
}

func (server *uiServer) serveAsset(response http.ResponseWriter, request *http.Request, name, contentType string) {
	if request.Method != http.MethodGet && request.Method != http.MethodHead {
		response.WriteHeader(http.StatusMethodNotAllowed)
		return
	}
	data, err := fs.ReadFile(server.assets, name)
	if err != nil {
		http.NotFound(response, request)
		return
	}
	response.Header().Set("Content-Type", contentType)
	if request.Method == http.MethodGet {
		_, _ = response.Write(data)
	}
}

func (server *uiServer) requireToken(response http.ResponseWriter, request *http.Request, next http.HandlerFunc) {
	const prefix = "Bearer "
	header := request.Header.Get("Authorization")
	if !strings.HasPrefix(header, prefix) ||
		subtle.ConstantTimeCompare([]byte(strings.TrimPrefix(header, prefix)), []byte(server.token)) != 1 {
		response.WriteHeader(http.StatusUnauthorized)
		return
	}
	next(response, request)
}

func (server *uiServer) validOrigin(request *http.Request) bool {
	if request.Header.Get("Sec-Fetch-Site") == "cross-site" {
		return false
	}
	origin := strings.TrimSpace(request.Header.Get("Origin"))
	return origin == "" || origin == server.origin
}

func loopbackRequest(request *http.Request) bool {
	host, _, err := net.SplitHostPort(request.RemoteAddr)
	remoteIP := net.ParseIP(host)
	if err != nil || remoteIP == nil || !remoteIP.IsLoopback() {
		return false
	}
	host = strings.TrimSpace(request.Host)
	if parsed, _, splitErr := net.SplitHostPort(host); splitErr == nil {
		host = parsed
	} else if strings.Contains(host, ":") && !(strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]")) {
		return false
	}
	ip := net.ParseIP(strings.Trim(host, "[]"))
	return ip != nil && ip.IsLoopback()
}

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

func setUISecurityHeaders(response http.ResponseWriter) {
	response.Header().Set("Cache-Control", "no-store")
	response.Header().Set("Content-Security-Policy", "default-src 'none'; script-src 'self'; style-src 'self'; connect-src 'self'; img-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'")
	response.Header().Set("Cross-Origin-Opener-Policy", "same-origin")
	response.Header().Set("Cross-Origin-Resource-Policy", "same-origin")
	response.Header().Set("Referrer-Policy", "no-referrer")
	response.Header().Set("X-Content-Type-Options", "nosniff")
	response.Header().Set("X-Frame-Options", "DENY")
}

func writeUIJSON(response http.ResponseWriter, value any) {
	response.Header().Set("Content-Type", "application/json; charset=utf-8")
	_ = json.NewEncoder(response).Encode(value)
}

func writeUIError(response http.ResponseWriter, err error, status int) {
	response.Header().Set("Content-Type", "application/json; charset=utf-8")
	response.WriteHeader(status)
	_ = json.NewEncoder(response).Encode(struct {
		Error string `json:"error"`
	}{Error: err.Error()})
}
