// Package localsecurity owns the local daemon Host, Origin, CORS, WebSocket,
// query-auth, and sensitive-URL redaction policy.
package localsecurity

import (
	"context"
	"fmt"
	"net"
	"net/http"
	"net/url"
	"strings"

	"github.com/gin-gonic/gin"
)

const (
	AppOrigin               = "app://agentteam"
	rendererPreflightMaxAge = "600"
)

var (
	rendererAllowedMethods = []string{
		http.MethodGet,
		http.MethodHead,
		http.MethodPost,
		http.MethodPut,
		http.MethodPatch,
		http.MethodDelete,
		http.MethodOptions,
	}
	rendererAllowedRequestHeaders = []string{
		"Authorization",
		"Content-Type",
		"If-Match",
		"Idempotency-Key",
		"X-AgentTeam-Desktop-Onboarding-Registration-Cutoff",
	}
	rendererExposedResponseHeaders = []string{
		"ETag",
		"X-Plan-Draft-ETag",
		"X-Binding-Revision-ETag",
		"X-AgentTeam-Workflow-Agent-Edit-Active-At-Archive",
		"Content-Disposition",
		"X-AgentTeam-Desktop-Auth-Error",
	}
)

type Options struct {
	Host       string
	DevOrigins []string
}

type Policy struct {
	host           string
	allowedOrigins map[string]struct{}
}

type validatedContextKey struct{}

func New(options Options) (*Policy, error) {
	host, err := normalizeHost(options.Host)
	if err != nil {
		return nil, err
	}
	origins := map[string]struct{}{AppOrigin: {}}
	for _, raw := range options.DevOrigins {
		origin, err := normalizeOrigin(raw)
		if err != nil {
			return nil, fmt.Errorf("invalid local development origin: %w", err)
		}
		origins[origin] = struct{}{}
	}
	return &Policy{host: host, allowedOrigins: origins}, nil
}

func (p *Policy) Middleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		if err := p.Validate(c.Request); err != nil {
			c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "Forbidden"})
			return
		}
		c.Request = c.Request.WithContext(
			context.WithValue(c.Request.Context(), validatedContextKey{}, true),
		)
		origin := strings.TrimSpace(c.GetHeader("Origin"))
		if origin != "" {
			c.Header("Access-Control-Allow-Origin", origin)
			c.Header("Access-Control-Expose-Headers", headerList(rendererExposedResponseHeaders))
			c.Header("Vary", "Origin")
		}
		if c.Request.Method == http.MethodOptions {
			if origin == "" {
				c.AbortWithStatus(http.StatusForbidden)
				return
			}
			c.Header("Access-Control-Allow-Methods", headerList(rendererAllowedMethods))
			c.Header("Access-Control-Allow-Headers", headerList(rendererAllowedRequestHeaders))
			c.Header("Access-Control-Max-Age", rendererPreflightMaxAge)
			c.AbortWithStatus(http.StatusNoContent)
			return
		}
		c.Next()
	}
}

func headerList(values []string) string {
	return strings.Join(values, ", ")
}

func (p *Policy) Validate(request *http.Request) error {
	if request == nil {
		return fmt.Errorf("request is nil")
	}
	if values := request.Header.Values("Host"); len(values) > 0 {
		return fmt.Errorf("duplicate Host header")
	}
	host, err := normalizeHost(request.Host)
	if err != nil || host != p.host {
		return fmt.Errorf("unexpected Host")
	}
	origins := request.Header.Values("Origin")
	if len(origins) == 0 {
		return nil
	}
	if len(origins) != 1 || strings.Contains(origins[0], ",") {
		return fmt.Errorf("Origin must appear exactly once")
	}
	origin, err := normalizeOrigin(origins[0])
	if err != nil {
		return err
	}
	if _, ok := p.allowedOrigins[origin]; !ok {
		return fmt.Errorf("Origin is not trusted")
	}
	return nil
}

func (p *Policy) ValidateWebSocket(request *http.Request) error {
	return p.Validate(request)
}

// ValidateWebSocketOrigin applies the shared origin parser to routers that do
// not install a process-bound Policy (mostly package tests and embedded
// callers). Production requests are already fenced by Policy.Middleware.
func ValidateWebSocketOrigin(request *http.Request) error {
	if request == nil {
		return fmt.Errorf("request is nil")
	}
	if validated, _ := request.Context().Value(validatedContextKey{}).(bool); validated {
		return nil
	}
	origins := request.Header.Values("Origin")
	if len(origins) == 0 {
		return nil
	}
	if len(origins) != 1 || strings.Contains(origins[0], ",") {
		return fmt.Errorf("Origin must appear exactly once")
	}
	raw := strings.TrimSpace(origins[0])
	if raw == AppOrigin {
		return nil
	}
	parsed, err := url.Parse(raw)
	if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") ||
		parsed.Host == "" || parsed.User != nil || parsed.Opaque != "" ||
		parsed.Path != "" || parsed.RawQuery != "" || parsed.Fragment != "" {
		return fmt.Errorf("Origin must be a pure http(s) origin or app://agentteam")
	}
	scheme := "http"
	if request.TLS != nil {
		scheme = "https"
	}
	if !strings.EqualFold(parsed.Scheme, scheme) ||
		!strings.EqualFold(parsed.Host, request.Host) {
		return fmt.Errorf("Origin does not match request authority")
	}
	return nil
}

func QuerySessionAllowed(method, path string) bool {
	switch method {
	case http.MethodGet, http.MethodHead:
	default:
		return false
	}
	if isWebSocketPath(path) {
		return true
	}
	if isDiagnosticArchivePath(path) {
		return true
	}
	segments := strings.Split(strings.Trim(path, "/"), "/")
	for _, segment := range segments {
		switch strings.ToLower(segment) {
		case "attachments", "attachment", "stickers", "sticker", "avatars", "avatar-assets", "avatar", "media":
			return true
		}
	}
	return false
}

func isDiagnosticArchivePath(path string) bool {
	segments := strings.Split(strings.Trim(path, "/"), "/")
	return len(segments) == 6 &&
		segments[0] == "api" &&
		segments[1] == "internal" &&
		segments[2] == "machines" &&
		strings.TrimSpace(segments[3]) != "" &&
		segments[4] == "diagnostics" &&
		segments[5] == "archive"
}

func ValidateQueryCredentials(request *http.Request) error {
	if request == nil || request.URL == nil {
		return fmt.Errorf("request URL is unavailable")
	}
	query := request.URL.Query()
	if _, ok := query["token"]; ok {
		return fmt.Errorf("legacy token query credential is forbidden")
	}
	values, ok := query["accessToken"]
	if !ok {
		return nil
	}
	if len(values) != 1 || strings.TrimSpace(values[0]) == "" {
		return fmt.Errorf("accessToken must appear exactly once")
	}
	if request.Header.Get("Authorization") != "" ||
		request.Header.Get("X-Session-Token") != "" ||
		request.Header.Get("X-Api-Key") != "" {
		return fmt.Errorf("duplicate header and query credentials")
	}
	if !QuerySessionAllowed(request.Method, request.URL.Path) {
		return fmt.Errorf("query session is not allowed on this route")
	}
	return nil
}

func RedactURL(raw string) string {
	parsed, err := url.Parse(raw)
	if err != nil {
		return "[REDACTED URL]"
	}
	query := parsed.Query()
	changed := false
	for key := range query {
		if strings.EqualFold(key, "accessToken") || strings.EqualFold(key, "token") {
			query.Set(key, "[REDACTED]")
			changed = true
		}
	}
	if changed {
		parsed.RawQuery = query.Encode()
	}
	return parsed.String()
}

func normalizeHost(raw string) (string, error) {
	raw = strings.TrimSpace(raw)
	if raw == "" || strings.Contains(raw, ",") || strings.ContainsAny(raw, " \t\r\n") {
		return "", fmt.Errorf("Host is malformed")
	}
	host, port, err := net.SplitHostPort(raw)
	if err != nil || port == "" {
		return "", fmt.Errorf("Host must contain an explicit port")
	}
	ip := net.ParseIP(host)
	if ip == nil || !ip.IsLoopback() {
		return "", fmt.Errorf("Host must be a numeric loopback address")
	}
	return net.JoinHostPort(ip.String(), port), nil
}

func normalizeOrigin(raw string) (string, error) {
	raw = strings.TrimSpace(raw)
	if raw == "" || raw == "null" || raw == "*" {
		return "", fmt.Errorf("Origin is malformed")
	}
	parsed, err := url.Parse(raw)
	if err != nil || parsed.Scheme == "" || parsed.Host == "" ||
		parsed.User != nil || parsed.Opaque != "" ||
		(parsed.Path != "" && parsed.Path != "/") ||
		parsed.RawQuery != "" || parsed.Fragment != "" {
		return "", fmt.Errorf("Origin must be a pure origin")
	}
	switch parsed.Scheme {
	case "app":
		if parsed.Host != "agentteam" || parsed.Path != "" {
			return "", fmt.Errorf("custom scheme origin is not trusted")
		}
		return AppOrigin, nil
	case "http", "https":
		host, err := normalizeHost(parsed.Host)
		if err != nil {
			return "", err
		}
		return strings.ToLower(parsed.Scheme) + "://" + host, nil
	default:
		return "", fmt.Errorf("Origin scheme is not trusted")
	}
}

func isWebSocketPath(path string) bool {
	if path == "/socket" || strings.HasPrefix(path, "/socket/") ||
		path == "/api/internal/servers/active/stream" ||
		path == "/api/v1/events/stream" {
		return true
	}
	return strings.HasPrefix(path, "/api/v1/workspaces/") &&
		strings.HasSuffix(path, "/events/stream")
}
