// Package workspaceurl owns the canonical AgentTeam outer URL seam for embedded
// AGS. Edge callers use Parse without mutation; the workspace server calls
// Strip only after placement has selected and verified the same workspace.
package workspaceurl

import (
	"errors"
	"net/http"
	"net/url"
	"strings"

	"github.com/tidbcloud/AgentTeam/internal/core/identifiers"
)

const outerPrefix = "/api/v1/workspaces/"

var ErrInvalidPath = errors.New("invalid workspace AGS path")

type Path struct {
	WorkspaceID string
	pathPrefix  string
	innerPath   string
	innerRaw    string
}

// Qualify moves one already-approved native AGS request behind the canonical
// workspace seam. Callers must classify and authorize the operation before
// invoking Qualify; this helper owns only lossless URL composition.
func Qualify(request *http.Request, workspaceID string) error {
	if request == nil || request.URL == nil || !identifiers.ValidWorkspaceID(workspaceID) || Owns(request) {
		return ErrInvalidPath
	}
	if request.RequestURI != "" && request.RequestURI != request.URL.RequestURI() {
		return ErrInvalidPath
	}
	if !validInnerBoundary(request.URL.Path) {
		return ErrInvalidPath
	}
	if request.URL.RawPath != "" {
		decoded, err := url.PathUnescape(request.URL.RawPath)
		if err != nil || decoded != request.URL.Path || !validInnerBoundary(request.URL.RawPath) {
			return ErrInvalidPath
		}
	}

	prefix := outerPrefix + workspaceID + "/ags"
	request.URL.Path = prefix + request.URL.Path
	if request.URL.RawPath != "" {
		request.URL.RawPath = prefix + request.URL.RawPath
	}
	if request.RequestURI != "" {
		request.RequestURI = request.URL.RequestURI()
	}
	return nil
}

// Owns reports whether the decoded path targets the workspace AGS namespace.
// It is intentionally broader than Parse so malformed aliases fail closed
// instead of falling through to a generic workspace or Central route.
func Owns(request *http.Request) bool {
	if request == nil || request.URL == nil || !strings.HasPrefix(request.URL.Path, outerPrefix) {
		return false
	}
	remainder := strings.TrimPrefix(request.URL.Path, outerPrefix)
	separator := strings.IndexByte(remainder, '/')
	if separator < 0 {
		return false
	}
	afterWorkspace := remainder[separator:]
	return afterWorkspace == "/ags" || strings.HasPrefix(afterWorkspace, "/ags/")
}

func Parse(request *http.Request) (Path, error) {
	if request == nil || request.URL == nil {
		return Path{}, ErrInvalidPath
	}
	if request.RequestURI != "" && request.RequestURI != request.URL.RequestURI() {
		return Path{}, ErrInvalidPath
	}
	if request.URL.RawPath != "" {
		decoded, err := url.PathUnescape(request.URL.RawPath)
		if err != nil || decoded != request.URL.Path {
			return Path{}, ErrInvalidPath
		}
	}
	if !strings.HasPrefix(request.URL.Path, outerPrefix) {
		return Path{}, ErrInvalidPath
	}
	remainder := strings.TrimPrefix(request.URL.Path, outerPrefix)
	separator := strings.IndexByte(remainder, '/')
	if separator <= 0 {
		return Path{}, ErrInvalidPath
	}
	workspaceID := remainder[:separator]
	if !identifiers.ValidWorkspaceID(workspaceID) {
		return Path{}, ErrInvalidPath
	}
	pathPrefix := outerPrefix + workspaceID + "/ags"
	if !pathMatchesPrefix(request.URL.Path, pathPrefix) {
		return Path{}, ErrInvalidPath
	}
	innerPath := strings.TrimPrefix(request.URL.Path, pathPrefix)
	if !validInnerBoundary(innerPath) {
		return Path{}, ErrInvalidPath
	}

	innerRaw := ""
	if request.URL.RawPath != "" {
		if !pathMatchesPrefix(request.URL.RawPath, pathPrefix) {
			return Path{}, ErrInvalidPath
		}
		innerRaw = strings.TrimPrefix(request.URL.RawPath, pathPrefix)
		if !validInnerBoundary(innerRaw) {
			return Path{}, ErrInvalidPath
		}
	}
	return Path{WorkspaceID: workspaceID, pathPrefix: pathPrefix, innerPath: innerPath, innerRaw: innerRaw}, nil
}

func (path Path) Strip(request *http.Request, routedWorkspaceID string) error {
	if request == nil || request.URL == nil || path.WorkspaceID == "" || routedWorkspaceID != path.WorkspaceID {
		return ErrInvalidPath
	}
	parsed, err := Parse(request)
	if err != nil || parsed != path {
		return ErrInvalidPath
	}
	originalRequestURI := request.RequestURI
	request.URL.Path = path.innerPath
	request.URL.RawPath = path.innerRaw
	if originalRequestURI != "" {
		request.RequestURI = request.URL.RequestURI()
	}
	return nil
}

// PublicBaseURL composes the request-owned public AGS base from deployment
// configuration and this already-parsed canonical path. Request authority and
// forwarding headers are intentionally not inputs.
func (path Path) PublicBaseURL(deploymentBase string) (string, error) {
	base, err := url.Parse(strings.TrimSpace(deploymentBase))
	if err != nil || base.Scheme == "" || base.Host == "" || path.pathPrefix == "" {
		return "", ErrInvalidPath
	}
	base.RawQuery = ""
	base.Fragment = ""
	base.Path = strings.TrimRight(base.Path, "/") + path.pathPrefix
	base.RawPath = ""
	return strings.TrimRight(base.String(), "/"), nil
}

func pathMatchesPrefix(path, prefix string) bool {
	return path == prefix || (strings.HasPrefix(path, prefix) && len(path) > len(prefix) && path[len(prefix)] == '/')
}

func validInnerBoundary(path string) bool {
	return len(path) > 1 && strings.HasPrefix(path, "/") && !strings.HasPrefix(path, "//")
}
