//go:generate go run main.go

package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"maps"
	"os"
	"path/filepath"
	"strings"

	"golang.org/x/text/cases"
	"golang.org/x/text/language"
)

const (
	// swagger json file: {SwaggerDir}/{service_name}/{version}/services.swagger.json
	// ui swagger json file: {SwaggerDir}/{service_name}/{version}/services.ui.swagger.json
	defaultSwaggerDirs = "../../spec/tidb_cloud_open_api,../../spec/bff"
)

var swaggerDirs string

func main() {
	// Parse command line flags to get swagger directories
	flag.StringVar(&swaggerDirs, "dirs", defaultSwaggerDirs, "swagger dir")
	flag.Parse()

	// Split comma-separated swagger directories
	swaggerDirList := strings.Split(swaggerDirs, ",")

	// Process each swagger directory
	for _, swaggerDir := range swaggerDirList {
		fs, err := os.ReadDir(swaggerDir)
		if err != nil {
			panic(err)
		}

		// Process each service in the directory
		for _, svc := range fs {
			// Skip non-directory entries
			if !svc.IsDir() {
				continue
			}

			fmt.Printf("generate %s %s ui swagger.\n", filepath.Base(swaggerDir), svc.Name())

			// Get all version directories for this service
			versions, err := GetVersions(filepath.Join(swaggerDir, svc.Name()))
			if err != nil {
				panic(err)
			}

			// Process each version of the service
			for _, version := range versions {
				// Skip the "common" service as it doesn't need UI swagger
				if svc.Name() == "common" {
					fmt.Printf("skip service %s %s\n", filepath.Base(swaggerDir), svc.Name())
					continue
				}

				// Handle "nextgen" service separately (has multiple swagger files per version)
				if svc.Name() == "nextgen" {
					fmt.Printf(
						"generate %s %s %s ui swagger for nextgen.\n",
						filepath.Base(swaggerDir),
						svc.Name(),
						version,
					)
					err := generateUISwaggerForNextgen(swaggerDir, svc.Name(), version)
					if err != nil {
						panic(err)
					}
					continue
				}

				// Process standard services (one swagger file per version)
				fmt.Printf("service: %s %s. version:  %s\n", filepath.Base(swaggerDir), svc.Name(), version)
				swaggerPath := filepath.Join(swaggerDir, svc.Name(), version, "services.swagger.json")
				swagger, err := ParseSwaggerFromFile(swaggerPath)
				if err != nil {
					panic(err)
				}

				var uiSwagger map[string]any
				basePath := fmt.Sprintf("/api/v2023/%s/%s", underscoreToUpperCamelCase(svc.Name()), version)
				// fs is served under the v2025 UI base path.
				if svc.Name() == "fs" && version == "v1beta2" {
					basePath = "/api/v2025/fs/v1beta2"
				}

				if svc.Name() == "cluster_management" && version == "v1beta2" {
					fmt.Printf("generateOneConsoleUISwagger")
					uiSwagger = generateOneConsoleUISwagger(svc.Name(),
						swagger,
						basePath,
					)
				} else {
					uiSwagger = generateUISwagger(
						svc.Name(),
						swagger,
						basePath,
					)
				}

				// Write the UI swagger to file
				s, err := json.MarshalIndent(uiSwagger, "", "  ")
				if err != nil {
					panic(err)
				}
				err = os.WriteFile(filepath.Join(swaggerDir, svc.Name(), version, "services.ui.swagger.json"), s, 0o600)
				if err != nil {
					panic(err)
				}
			}
		}
	}
}

// generateUISwaggerForNextgen processes nextgen service which has multiple swagger files per version.
// Each swagger file is converted to a corresponding UI swagger file.
func generateUISwaggerForNextgen(swaggerDir, svcName, version string) error {
	// Find all *.swagger.json files in the nextgen version directory
	nextgenDir := filepath.Join(swaggerDir, svcName, version)
	nextgenFiles, err := os.ReadDir(nextgenDir)
	if err != nil {
		return err
	}

	// Process each file in the directory
	for _, file := range nextgenFiles {
		// Skip directories
		if file.IsDir() {
			continue
		}

		// Process only *.swagger.json files, but skip already generated *.ui.swagger.json files
		if !strings.HasSuffix(file.Name(), ".swagger.json") || strings.HasSuffix(file.Name(), ".ui.swagger.json") {
			continue
		}

		fmt.Printf("generate %s %s %s ui swagger for nextgen.\n", filepath.Base(swaggerDir), svcName, file.Name())

		// Parse the swagger file
		swaggerPath := filepath.Join(nextgenDir, file.Name())
		swagger, err := ParseSwaggerFromFile(swaggerPath)
		if err != nil {
			return err
		}

		// Generate UI swagger with v2025 API path
		uiSwagger := generateUISwagger(svcName, swagger, fmt.Sprintf("/api/v2025/cloud/%s", version))

		// Marshal to JSON with indentation
		s, err := json.MarshalIndent(uiSwagger, "", "  ")
		if err != nil {
			return err
		}

		// Write to file with name pattern: {original_name}.ui.swagger.json
		err = os.WriteFile(
			filepath.Join(
				swaggerDir,
				svcName,
				version,
				fmt.Sprintf("%s.ui.swagger.json", strings.TrimSuffix(file.Name(), ".swagger.json")),
			),
			s,
			0o600,
		)
		if err != nil {
			return err
		}
	}
	return nil
}

// generateUISwagger transforms a standard swagger specification into a UI-specific version.
// It modifies the swagger by:
// 1. Setting the basePath to the provided value
// 2. Injecting orgId and projectId parameters into all API endpoints (if not already present)
// 3. Renaming cluster.clusterId to clusterId in parameter names and paths
func generateUISwagger(svcName string, swagger map[string]any, basePath string) map[string]any {
	// Set the base path for all API endpoints
	swagger["basePath"] = basePath

	// Extract the paths section from swagger, or create empty if not present
	swaggerPaths, ok := swagger["paths"].(map[string]any)
	if !ok {
		swaggerPaths = make(map[string]any)
	}

	// Extract version from swagger info for logging purposes
	var version string
	if info, ok := swagger["info"].(map[string]any); ok {
		if v, ok := info["version"].(string); ok {
			version = v
		}
	}

	// Create new paths map to store modified paths
	paths := make(map[string]any, len(swaggerPaths))

	// Process each API path
	for path, methodsAny := range swaggerPaths {
		// Extract methods (GET, POST, etc.) for this path
		methods, ok := methodsAny.(map[string]any)
		if !ok {
			continue
		}

		// Create new methods map for this path
		newMethods := make(map[string]any, len(methods))

		// Process each HTTP method for this path
		for method, mAny := range methods {
			// Extract method definition
			m, ok := mAny.(map[string]any)
			if !ok {
				continue
			}

			fmt.Printf(
				"generate %s ui swagger. api_version: %s. path: %s. method: %s\n",
				svcName,
				version,
				path,
				method,
			)

			// Extract existing parameters from the method
			var existingParams []any
			if paramsAny, ok := m["parameters"]; ok {
				if params, ok := paramsAny.([]any); ok {
					existingParams = params
				}
			}

			// Process parameters: filter out optional orgId/projectId and prepare list
			parameters := make([]map[string]any, 0, len(existingParams))
			hasOrgID := false
			hasProjectID := false

			// Iterate through existing parameters
			for _, pAny := range existingParams {
				p, ok := pAny.(map[string]any)
				if !ok {
					continue
				}

				// Extract parameter properties with type checking
				pName, ok := p["name"].(string)
				if !ok {
					continue
				}
				pIn, ok := p["in"].(string)
				if !ok {
					continue
				}
				pRequired, ok := p["required"].(bool)
				if !ok {
					pRequired = false // default to false if not specified or wrong type
				}

				// Check if orgId is already present as a required query parameter
				if pName == "orgId" && pIn == "query" {
					if pRequired {
						hasOrgID = true
					}
					// Remove it - we'll add our own version later
					continue
				}

				// Check if projectId is already present as a required query parameter
				if pName == "projectId" && pIn == "query" {
					if pRequired {
						hasProjectID = true
					}
					// Remove it - we'll add our own version later
					continue
				}

				// Rename cluster.clusterId to clusterId for update cluster operations
				if pName == "cluster.clusterId" {
					p["name"] = "clusterId"
				}

				// Keep this parameter
				parameters = append(parameters, p)
			}

			// Special case: serverless service /regions endpoint doesn't need projectId
			switch path {
			case "/regions":
				if svcName == "serverless" {
					hasProjectID = true
				}
			default:
			}

			// Add orgId parameter if not already present
			if !hasOrgID {
				parameters = append(parameters, map[string]any{
					"name":        "orgId",
					"description": "Organization id",
					"in":          "query",
					"required":    true,
					"type":        "string",
					"format":      "uint64",
				})
			}

			// Add projectId parameter if not already present
			if !hasProjectID {
				parameters = append(parameters, map[string]any{
					"name":        "projectId",
					"description": "Project id",
					"in":          "query",
					"required":    true,
					"type":        "string",
					"format":      "uint64",
				})
			}

			// Update method with modified parameters
			m["parameters"] = parameters
			newMethods[method] = m
		}

		// Replace cluster.clusterId with clusterId in the path itself
		path = strings.ReplaceAll(path, "cluster.clusterId", "clusterId")

		// Construct the full path with base path
		newPath := filepath.Join(basePath, path)

		// If this path already exists, merge the methods
		if existingMethods, ok := paths[newPath].(map[string]any); ok {
			maps.Copy(existingMethods, newMethods)
		} else {
			// Otherwise, add new path with its methods
			paths[newPath] = newMethods
		}
	}

	// Update swagger with modified paths
	swagger["paths"] = paths
	return swagger
}

// generateOneConsoleUISwagger transforms a standard swagger specification into a UI-specific version.
// It modifies the swagger by:
// 1. Setting the basePath to the provided value
// 2. Injecting orgId parameter into all API endpoints (if not already present)
func generateOneConsoleUISwagger(svcName string, swagger map[string]any, basePath string) map[string]any {
	// Set the base path for all API endpoints
	swagger["basePath"] = basePath

	// Extract the paths section from swagger, or create empty if not present
	swaggerPaths, ok := swagger["paths"].(map[string]any)
	if !ok {
		swaggerPaths = make(map[string]any)
	}

	// Extract version from swagger info for logging purposes
	var version string
	if info, ok := swagger["info"].(map[string]any); ok {
		if v, ok := info["version"].(string); ok {
			version = v
		}
	}

	// Create new paths map to store modified paths
	paths := make(map[string]any, len(swaggerPaths))

	// Process each API path
	for path, methodsAny := range swaggerPaths {
		// Extract methods (GET, POST, etc.) for this path
		methods, ok := methodsAny.(map[string]any)
		if !ok {
			continue
		}

		// Create new methods map for this path
		newMethods := make(map[string]any, len(methods))

		// Process each HTTP method for this path
		for method, mAny := range methods {
			// Extract method definition
			m, ok := mAny.(map[string]any)
			if !ok {
				continue
			}

			fmt.Printf(
				"generate %s ui swagger. api_version: %s. path: %s. method: %s\n",
				svcName,
				version,
				path,
				method,
			)

			// Extract existing parameters from the method
			var existingParams []any
			if paramsAny, ok := m["parameters"]; ok {
				if params, ok := paramsAny.([]any); ok {
					existingParams = params
				}
			}

			// Process parameters: filter out optional orgId and prepare list
			parameters := make([]map[string]any, 0, len(existingParams))
			hasOrgID := false

			// Iterate through existing parameters
			for _, pAny := range existingParams {
				p, ok := pAny.(map[string]any)
				if !ok {
					continue
				}

				// Extract parameter properties with type checking
				pName, ok := p["name"].(string)
				if !ok {
					continue
				}
				pIn, ok := p["in"].(string)
				if !ok {
					continue
				}
				pRequired, ok := p["required"].(bool)
				if !ok {
					pRequired = false // default to false if not specified or wrong type
				}

				// Check if orgId is already present as a required query parameter
				if pName == "orgId" && pIn == "query" {
					if pRequired {
						hasOrgID = true
					}
					// Remove it - we'll add our own version later
					continue
				}

				// Keep this parameter
				parameters = append(parameters, p)
			}

			// Add orgId parameter if not already present
			if !hasOrgID {
				parameters = append(parameters, map[string]any{
					"name":        "orgId",
					"description": "Organization id",
					"in":          "query",
					"required":    true,
					"type":        "string",
					"format":      "uint64",
				})
			}

			// Update method with modified parameters
			m["parameters"] = parameters
			newMethods[method] = m
		}

		// Replace cluster.clusterId with clusterId in the path itself
		path = strings.ReplaceAll(path, "cluster.clusterId", "clusterId")

		// Construct the full path with base path
		newPath := filepath.Join(basePath, path)

		// If this path already exists, merge the methods
		if existingMethods, ok := paths[newPath].(map[string]any); ok {
			maps.Copy(existingMethods, newMethods)
		} else {
			// Otherwise, add new path with its methods
			paths[newPath] = newMethods
		}
	}

	// Update swagger with modified paths
	swagger["paths"] = paths
	return swagger
}

// GetVersions returns all version directories for a given service.
// It scans the service directory and returns only subdirectories (versions).
func GetVersions(svcPath string) ([]string, error) {
	fs, err := os.ReadDir(svcPath)
	if err != nil {
		return nil, err
	}

	var versions []string
	for _, v := range fs {
		// Only include directories (each directory represents a version)
		if v.IsDir() {
			versions = append(versions, v.Name())
		}
	}

	return versions, nil
}

// ParseSwaggerFromFile reads a swagger JSON file and parses it into a map.
// Returns the parsed swagger object or an error if reading/parsing fails.
func ParseSwaggerFromFile(path string) (map[string]any, error) {
	file, err := os.Open(path)
	if err != nil {
		return nil, fmt.Errorf("failed to open: %w", err)
	}
	defer file.Close()
	content, err := io.ReadAll(file)
	if err != nil {
		return nil, fmt.Errorf("failed to read: %w", err)
	}
	return parseSwagger(content)
}

// parseSwagger parses swagger JSON bytes into a map structure.
// Returns the parsed swagger object or an error if unmarshaling fails.
func parseSwagger(content []byte) (map[string]any, error) {
	var swagger map[string]any

	err := json.Unmarshal(content, &swagger)
	if err != nil {
		return nil, fmt.Errorf("failed to unmarshal: %w", err)
	}

	return swagger, nil
}

// underscoreToUpperCamelCase converts underscore-separated strings to upper camel case.
// Examples: "hello_world" -> "helloWorld", "my_api_service" -> "myApiService"
// The first part is kept as-is, subsequent parts are title-cased.
func underscoreToUpperCamelCase(s string) string {
	// Remove any spaces
	s = strings.ReplaceAll(s, " ", "")

	// Split by underscore
	parts := strings.Split(s, "_")
	if len(parts) == 0 {
		return ""
	}

	var result strings.Builder
	// Keep the first part as-is (lowercase)
	_, _ = result.WriteString(parts[0])

	// Title-case the remaining parts
	c := cases.Title(language.English)
	for _, part := range parts[1:] {
		_, _ = result.WriteString(c.String(part))
	}

	return result.String()
}
