package server

import (
	"encoding/json"
	"log/slog"
	"regexp"
	"sort"
	"strings"
	"sync"

	"kra/internal/service"

	"github.com/gin-gonic/gin"
	swaggerFiles "github.com/swaggo/files"
	ginSwagger "github.com/swaggo/gin-swagger"
	"github.com/swaggo/swag"
)

const swaggerInstanceName = "kra-admin"

var (
	swaggerPathParameter = regexp.MustCompile(`:([A-Za-z0-9_]+)`)
	swaggerRegistration  sync.Once
	swaggerDocument      runtimeSwaggerDocument
)

type runtimeSwaggerDocument struct {
	mu  sync.RWMutex
	doc string
}

func (d *runtimeSwaggerDocument) ReadDoc() string {
	d.mu.RLock()
	defer d.mu.RUnlock()
	return d.doc
}

func (d *runtimeSwaggerDocument) replace(doc string) {
	d.mu.Lock()
	d.doc = doc
	d.mu.Unlock()
}

func registerSwagger(engine *gin.Engine, prefix, version string, logger *slog.Logger) {
	swaggerRegistration.Do(func() { swag.Register(swaggerInstanceName, &swaggerDocument) })
	swaggerDocument.replace(buildSwaggerDocument(engine.Routes(), prefix, version))
	path := strings.TrimSuffix(prefix, "/") + "/swagger/*any"
	engine.GET(path, ginSwagger.WrapHandler(
		swaggerFiles.Handler,
		ginSwagger.InstanceName(swaggerInstanceName),
		ginSwagger.URL("doc.json"),
		ginSwagger.PersistAuthorization(true),
	))
	if logger != nil {
		logger.Info("register swagger handler", "mod", "system", "path", path)
	}
}

func buildSwaggerDocument(routes []gin.RouteInfo, prefix, version string) string {
	basePath := strings.TrimSuffix(prefix, "/")
	if basePath == "" {
		basePath = "/"
	}
	sort.Slice(routes, func(i, j int) bool {
		if routes[i].Path == routes[j].Path {
			return routes[i].Method < routes[j].Method
		}
		return routes[i].Path < routes[j].Path
	})
	paths := make(map[string]map[string]any, len(routes))
	for _, route := range routes {
		if strings.HasSuffix(route.Path, "/*filepath") {
			continue
		}
		method := strings.ToLower(route.Method)
		switch method {
		case "get", "post", "put", "delete", "patch":
		default:
			continue
		}
		apiPath := route.Path
		if prefix != "" {
			apiPath = strings.TrimPrefix(apiPath, strings.TrimSuffix(prefix, "/"))
		}
		if apiPath == "" {
			apiPath = "/"
		}
		documentPath := swaggerPathParameter.ReplaceAllString(apiPath, `{$1}`)
		group, description := service.RouteMetadata(route.Method, apiPath)
		if description == "" {
			description = route.Method + " " + apiPath
		}
		operation := map[string]any{
			"tags":        []string{group},
			"summary":     description,
			"operationId": swaggerOperationID(route.Method, apiPath),
			"produces":    []string{"application/json"},
			"responses": map[string]any{
				"200": map[string]any{"description": "OK", "schema": map[string]any{"$ref": "#/definitions/Response"}},
			},
		}
		if parameters := swaggerPathParameters(apiPath); len(parameters) > 0 {
			operation["parameters"] = parameters
		}
		if !swaggerPublicPath(apiPath) {
			operation["security"] = []map[string][]string{{"ApiKeyAuth": {}}}
		}
		if paths[documentPath] == nil {
			paths[documentPath] = map[string]any{}
		}
		paths[documentPath][method] = operation
	}
	document := map[string]any{
		"swagger":  "2.0",
		"info":     map[string]any{"title": "Kra Administration API", "version": version},
		"basePath": basePath,
		"schemes":  []string{"http", "https"},
		"consumes": []string{"application/json"},
		"produces": []string{"application/json"},
		"securityDefinitions": map[string]any{
			"ApiKeyAuth": map[string]any{"type": "apiKey", "name": "x-token", "in": "header"},
		},
		"paths": paths,
		"definitions": map[string]any{
			"Response": map[string]any{
				"type": "object",
				"properties": map[string]any{
					"code": map[string]any{"type": "integer"},
					"data": map[string]any{"type": "object"},
					"msg":  map[string]any{"type": "string"},
				},
			},
		},
	}
	raw, _ := json.Marshal(document)
	return string(raw)
}

func swaggerOperationID(method, path string) string {
	value := strings.ToLower(method) + "_" + strings.Trim(path, "/")
	value = swaggerPathParameter.ReplaceAllString(value, "$1")
	return strings.Map(func(r rune) rune {
		if r >= 'a' && r <= 'z' || r >= '0' && r <= '9' || r == '_' {
			return r
		}
		return '_'
	}, value)
}

func swaggerPathParameters(path string) []map[string]any {
	matches := swaggerPathParameter.FindAllStringSubmatch(path, -1)
	parameters := make([]map[string]any, 0, len(matches))
	for _, match := range matches {
		parameters = append(parameters, map[string]any{"name": match[1], "in": "path", "required": true, "type": "string"})
	}
	return parameters
}

func swaggerPublicPath(path string) bool {
	for _, marker := range []string{
		"/health", "/base/login", "/base/captcha", "/init/checkdb", "/init/initdb",
		"/api/freshCasbin", "/sysExportTemplate/exportExcelByToken", "/sysExportTemplate/exportTemplateByToken",
		"/sysError/createSysError", "/info/getInfoDataSource", "/info/getInfoPublic",
	} {
		if path == marker {
			return true
		}
	}
	return false
}
