package api

import (
	"encoding/json"
	"net/http"
	"regexp"
	"strings"
	"sync"
	"unicode"

	"github.com/gin-gonic/gin"
	swaggerfiles "github.com/swaggo/files"
	ginswagger "github.com/swaggo/gin-swagger"
	"github.com/swaggo/swag"

	_ "nudgebee/runbook/docs/swagger" // generated by `make swag`; init registers the spec
)

// setupSwagger registers the Swagger UI under /swagger/index.html and the
// merged OpenAPI 2.0 document under /openapi.json. The merged document
// combines godoc-annotated handlers (rich entries) with auto-discovered
// stubs for every other route registered on the gin engine.
func (s *Server) setupSwagger() {
	s.router.GET("/openapi.json", mergedOpenAPIHandler(s.router))
	s.router.GET("/swagger/*any", ginswagger.WrapHandler(
		swaggerfiles.Handler,
		ginswagger.URL("/openapi.json"),
	))
}

// mergedOpenAPIHandler returns the swag-generated spec augmented with stub
// entries for every gin route swag did not document. Built lazily on first
// request, then cached for the process lifetime — routes are fixed after
// startup so the cache never invalidates.
func mergedOpenAPIHandler(r *gin.Engine) gin.HandlerFunc {
	var (
		once   sync.Once
		cached []byte
		merr   error
	)
	return func(c *gin.Context) {
		once.Do(func() {
			cached, merr = buildMergedOpenAPI(r)
		})
		if merr != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": merr.Error()})
			return
		}
		c.Data(http.StatusOK, "application/json; charset=utf-8", cached)
	}
}

func buildMergedOpenAPI(r *gin.Engine) ([]byte, error) {
	doc, err := swag.ReadDoc()
	if err != nil {
		return nil, err
	}

	var spec map[string]interface{}
	if err := json.Unmarshal([]byte(doc), &spec); err != nil {
		return nil, err
	}

	paths, _ := spec["paths"].(map[string]interface{})
	if paths == nil {
		paths = map[string]interface{}{}
		spec["paths"] = paths
	}

	for _, route := range r.Routes() {
		swPath := ginPathToSwagger(route.Path)
		method := strings.ToLower(route.Method)

		if strings.HasPrefix(swPath, "/swagger/") || swPath == "/openapi.json" {
			continue
		}

		methods, ok := paths[swPath].(map[string]interface{})
		if !ok {
			methods = map[string]interface{}{}
			paths[swPath] = methods
		}
		if _, alreadyDocumented := methods[method]; alreadyDocumented {
			continue
		}
		methods[method] = stubOperation(route.Handler, swPath, method)
	}

	injectScopeHeaders(paths)

	return json.Marshal(spec)
}

// injectScopeHeaders adds tenant/user/account header parameters to every
// operation that doesn't already declare them. Skips public-bypass paths
// (health, swagger, openapi spec).
func injectScopeHeaders(paths map[string]interface{}) {
	scopeHeaders := []map[string]interface{}{
		{
			"name":        "x-tenant-id",
			"in":          "header",
			"required":    false,
			"type":        "string",
			"description": "Scope the call to a tenant. Falls back to `tenant_id` session variable if unset.",
		},
		{
			"name":        "x-user-id",
			"in":          "header",
			"required":    false,
			"type":        "string",
			"description": "Scope the call to a user. Falls back to `user_id` session variable if unset.",
		},
		{
			"name":        "X-Account-ID",
			"in":          "header",
			"required":    false,
			"type":        "string",
			"description": "Cloud account ID, read by handlers that operate on per-account resources.",
		},
	}

	for swPath, raw := range paths {
		if isPublicPath(swPath) {
			continue
		}
		methods, ok := raw.(map[string]interface{})
		if !ok {
			continue
		}
		for _, op := range methods {
			opMap, ok := op.(map[string]interface{})
			if !ok {
				continue
			}
			existing, _ := opMap["parameters"].([]interface{})
			seen := map[string]bool{}
			for _, p := range existing {
				if pm, ok := p.(map[string]interface{}); ok {
					if pm["in"] == "header" {
						if name, _ := pm["name"].(string); name != "" {
							seen[strings.ToLower(name)] = true
						}
					}
				}
			}
			for _, h := range scopeHeaders {
				if seen[strings.ToLower(h["name"].(string))] {
					continue
				}
				existing = append(existing, h)
			}
			opMap["parameters"] = existing
		}
	}
}

func isPublicPath(p string) bool {
	if p == "/health" || p == "/openapi.json" {
		return true
	}
	return strings.HasPrefix(p, "/swagger/")
}

// ginParamRe matches gin's `:param` and `*param` placeholders.
var ginParamRe = regexp.MustCompile(`[:*]([A-Za-z0-9_]+)`)

// swaggerParamRe matches OpenAPI 2.0 style `{param}` placeholders.
var swaggerParamRe = regexp.MustCompile(`\{([A-Za-z0-9_]+)\}`)

// ginPathToSwagger converts gin path style (`/workflows/:id/runs/:execution_id`)
// to OpenAPI 2.0 style (`/workflows/{id}/runs/{execution_id}`).
func ginPathToSwagger(p string) string {
	return ginParamRe.ReplaceAllString(p, "{$1}")
}

// summaryFromPath derives a human-readable operation summary from the route
// path's last non-placeholder segment.
func summaryFromPath(p string) string {
	segs := strings.Split(strings.Trim(p, "/"), "/")
	if len(segs) == 0 {
		return p
	}
	last := segs[len(segs)-1]
	if strings.HasPrefix(last, "{") {
		if len(segs) >= 2 {
			last = segs[len(segs)-2]
		} else {
			return p
		}
	}
	last = strings.ReplaceAll(last, "-", " ")
	last = strings.ReplaceAll(last, "_", " ")
	parts := strings.Fields(last)
	for i, w := range parts {
		if w == "" {
			continue
		}
		runes := []rune(w)
		runes[0] = unicode.ToUpper(runes[0])
		parts[i] = string(runes)
	}
	return strings.Join(parts, " ")
}

// funcSuffixRe matches Go's auto-generated closure suffixes like `.func25`,
// `.func1.2`, `.funcN.M.K`.
var funcSuffixRe = regexp.MustCompile(`(\.func\d+)+(\.\d+)*$`)

// cleanHandlerName strips the module path and Go's auto-generated `.funcN`
// closure suffixes so a handler reported by gin as
// `nudgebee/runbook/api.(*Server).createWorkflow-fm` reads as
// `(*Server).createWorkflow-fm`.
func cleanHandlerName(name string) string {
	short := name
	if idx := strings.LastIndex(short, "/"); idx != -1 {
		short = short[idx+1:]
	}
	if dot := strings.Index(short, "."); dot != -1 {
		short = short[dot+1:]
	}
	stripped := funcSuffixRe.ReplaceAllString(short, "")
	if stripped == "" || strings.HasPrefix(stripped, "func") {
		return short
	}
	return stripped
}

// stubOperation builds a minimal Operation object for a route swag did not
// document. Stubs are tagged "internal" so they group together in the UI;
// for write methods, a free-form JSON body editor is added so Try-it-out is
// usable without per-handler annotations.
func stubOperation(handlerName, path, method string) map[string]interface{} {
	op := map[string]interface{}{
		"tags":        []string{"internal"},
		"summary":     summaryFromPath(path),
		"description": "Registered inside `" + cleanHandlerName(handlerName) + "`.",
		"consumes":    []string{"application/json"},
		"produces":    []string{"application/json"},
		"security": []map[string]interface{}{
			{"ActionToken": []string{}},
		},
		"responses": map[string]interface{}{
			"default": map[string]interface{}{
				"description": "Response shape depends on the handler.",
			},
		},
	}

	params := []interface{}{}

	// `path` arrives in OpenAPI 2.0 form (gin params already converted via
	// ginPathToSwagger), so use swaggerParamRe to extract `{name}` segments.
	matches := swaggerParamRe.FindAllStringSubmatch(path, -1)
	seen := map[string]bool{}
	for _, m := range matches {
		name := m[1]
		if seen[name] {
			continue
		}
		seen[name] = true
		params = append(params, map[string]interface{}{
			"name":     name,
			"in":       "path",
			"required": true,
			"type":     "string",
		})
	}

	switch method {
	case "post", "put", "patch", "delete":
		params = append(params, map[string]interface{}{
			"name":        "body",
			"in":          "body",
			"required":    false,
			"description": "Free-form JSON body. RPC action endpoints expect the standard envelope `{action:{name}, input:{...}, session_variables:{...}}`; other endpoints have handler-specific shapes.",
			"schema": map[string]interface{}{
				"type":                 "object",
				"additionalProperties": true,
			},
		})
	}

	if len(params) > 0 {
		op["parameters"] = params
	}

	return op
}
