// Package handlers — openapi.go
//
// with extension-declared API routes. Extensions with api_schema get rich
// path items; others get auto-generated stubs.
//
// Two tiers:
//   Tier 1 — Stubs: zero extension author effort, every route gets a stub
//   Tier 2 — api_schema: optional manifest field for richer docs
package handlers

import (
	"context"
	"fmt"
	"log"
	"strings"

	"armature/store"

	"gopkg.in/yaml.v3"
)

// BuildOpenAPISpec produces a complete OpenAPI 3.0 spec by merging the static
// kernel spec with dynamically discovered extension routes.
//
// staticSpec is the raw YAML of the kernel's openapi.yaml.
// version is substituted into the spec's info.version field.
func BuildOpenAPISpec(stores store.Stores, staticSpec []byte, version string) (map[string]any, error) {
	// 1. Parse static spec
	spec := make(map[string]any)
	patched := strings.Replace(string(staticSpec), "${VERSION}", version, 1)
	if err := yaml.Unmarshal([]byte(patched), &spec); err != nil {
		return nil, fmt.Errorf("failed to parse static OpenAPI spec: %w", err)
	}

	// Ensure paths map exists
	paths, _ := spec["paths"].(map[string]any)
	if paths == nil {
		paths = make(map[string]any)
		spec["paths"] = paths
	}

	// Ensure tags array exists
	tags, _ := spec["tags"].([]any)

	// 2. Enumerate enabled extensions with API routes
	if stores.Packages == nil {
		return spec, nil
	}

	pkgs, err := stores.Packages.List(context.Background())
	if err != nil {
		log.Printf("[openapi] Failed to list packages: %v", err)
		return spec, nil // degrade gracefully — return kernel-only spec
	}

	for _, pkg := range pkgs {
		if !pkg.Enabled || pkg.Manifest == nil {
			continue
		}
		if !HasAPIRoutes(pkg.Manifest) {
			continue
		}

		slug := pkg.ID
		tagName := "extensions/" + slug

		// Add extension tag
		tags = append(tags, map[string]any{
			"name":        tagName,
			"description": fmt.Sprintf("API routes for %s extension", pkg.Title),
		})

		// Build schema lookup from api_schema (if present)
		schemaEntries := ParseAPISchema(pkg.Manifest)
		schemaMap := make(map[string]APISchemaEntry) // key: "METHOD /path"
		for _, entry := range schemaEntries {
			key := entry.Method + " " + entry.Path
			schemaMap[key] = entry
		}

		// Iterate declared routes
		routes := ExtractAPIRoutes(pkg.Manifest)
		if len(routes) == 0 {
			// api_routes: true — no specific routes to enumerate.
			// If api_schema is declared, generate entries from those.
			for _, entry := range schemaEntries {
				oaPath := fmt.Sprintf("/s/%s/api%s", slug, entry.Path)
				method := strings.ToLower(entry.Method)
				pathItem := getOrCreatePathItem(paths, oaPath)
				pathItem[method] = buildSchemaOperation(entry, tagName)
			}
			continue
		}

		for _, route := range routes {
			// Skip wildcard paths — can't represent meaningfully in OpenAPI
			if strings.HasSuffix(route.Path, "*") {
				continue
			}

			oaPath := fmt.Sprintf("/s/%s/api%s", slug, route.Path)
			method := strings.ToLower(route.Method)

			// Wildcard method: generate for common methods
			methods := []string{method}
			if route.Method == "*" {
				methods = []string{"get", "post", "put", "delete", "patch"}
			}

			for _, m := range methods {
				pathItem := getOrCreatePathItem(paths, oaPath)
				key := strings.ToUpper(m) + " " + route.Path
				if schema, ok := schemaMap[key]; ok {
					pathItem[m] = buildSchemaOperation(schema, tagName)
				} else {
					pathItem[m] = buildStubOperation(slug, m, route.Path, tagName)
				}
			}
		}
	}

	spec["tags"] = tags
	return spec, nil
}

// getOrCreatePathItem returns the path item map for the given OpenAPI path,
// creating it if it doesn't exist.
func getOrCreatePathItem(paths map[string]any, path string) map[string]any {
	if existing, ok := paths[path].(map[string]any); ok {
		return existing
	}
	item := make(map[string]any)
	paths[path] = item
	return item
}

// buildStubOperation generates an auto-generated stub operation for routes
// without api_schema declarations.
func buildStubOperation(slug, method, path, tag string) map[string]any {
	return map[string]any{
		"summary": fmt.Sprintf("%s: %s %s", slug, strings.ToUpper(method), path),
		"tags":    []any{tag},
		"responses": map[string]any{
			"200": map[string]any{
				"description": "Extension-defined response",
			},
		},
	}
}

// buildSchemaOperation converts an APISchemaEntry into a full OpenAPI operation.
func buildSchemaOperation(entry APISchemaEntry, tag string) map[string]any {
	op := map[string]any{
		"summary": entry.Summary,
		"tags":    []any{tag},
	}
	if entry.Summary == "" {
		op["summary"] = fmt.Sprintf("%s %s", entry.Method, entry.Path)
	}
	if entry.Description != "" {
		op["description"] = entry.Description
	}

	// Query/path parameters
	if len(entry.Params) > 0 {
		var params []any
		for name, def := range entry.Params {
			param := map[string]any{
				"name": name,
				"in":   "query",
			}
			if m, ok := def.(map[string]any); ok {
				if t, ok := m["type"].(string); ok {
					param["schema"] = map[string]any{"type": mapJSONType(t)}
				}
				if desc, ok := m["description"].(string); ok {
					param["description"] = desc
				}
				if req, ok := m["required"].(bool); ok && req {
					param["required"] = true
				}
			} else {
				// Simple type string
				if t, ok := def.(string); ok {
					param["schema"] = map[string]any{"type": mapJSONType(t)}
				}
			}
			params = append(params, param)
		}
		op["parameters"] = params
	}

	// Request body
	if len(entry.Body) > 0 {
		properties := make(map[string]any)
		var required []any
		for name, def := range entry.Body {
			prop := map[string]any{}
			if m, ok := def.(map[string]any); ok {
				if t, ok := m["type"].(string); ok {
					prop["type"] = mapJSONType(t)
				}
				if desc, ok := m["description"].(string); ok {
					prop["description"] = desc
				}
				if req, ok := m["required"].(bool); ok && req {
					required = append(required, name)
				}
			} else if t, ok := def.(string); ok {
				prop["type"] = mapJSONType(t)
			}
			properties[name] = prop
		}
		schema := map[string]any{
			"type":       "object",
			"properties": properties,
		}
		if len(required) > 0 {
			schema["required"] = required
		}
		op["requestBody"] = map[string]any{
			"required": true,
			"content": map[string]any{
				"application/json": map[string]any{
					"schema": schema,
				},
			},
		}
	}

	// Response
	resp := map[string]any{
		"200": map[string]any{
			"description": "Successful response",
		},
	}
	if entry.Response != nil {
		respSchema := make(map[string]any)
		if t, ok := entry.Response["type"].(string); ok {
			respSchema["type"] = mapJSONType(t)
		}
		if ex, ok := entry.Response["example"]; ok {
			respSchema["example"] = ex
		}
		resp["200"] = map[string]any{
			"description": "Successful response",
			"content": map[string]any{
				"application/json": map[string]any{
					"schema": respSchema,
				},
			},
		}
	}
	op["responses"] = resp

	return op
}

// mapJSONType converts common type names to OpenAPI types.
func mapJSONType(t string) string {
	switch strings.ToLower(t) {
	case "int", "integer":
		return "integer"
	case "float", "number", "double":
		return "number"
	case "bool", "boolean":
		return "boolean"
	case "array", "list":
		return "array"
	case "object", "map", "dict":
		return "object"
	default:
		return "string"
	}
}
