// Package openapi builds OpenAPI 3.0.3 documents for the drive9 HTTP API.
//
// The package contains no HTTP handlers: it only assembles documents from an
// endpoint registry (defined in pkg/server) plus a lightweight Go-struct →
// JSON Schema reflector. The reflector keeps request/response schemas in sync
// with the Go types handlers actually encode, so the generated documents do
// not drift from the implementation.
package openapi

import (
	"encoding/json"
	"fmt"
	"reflect"
	"strings"
	"time"
)

// jsonSchema is the JSON Schema subset emitted by the reflector. It is
// assembled as a plain map so the final document marshals with
// encoding/json.
type jsonSchema map[string]any

// SchemaRef references a schema for a request body or a response. Either Type
// (reflected from a Go type) or Raw (hand-written schema) must be set.
type SchemaRef struct {
	// Type is the Go type reflected into a JSON Schema. Anonymous structs
	// are inlined; named types become $ref entries in components/schemas.
	Type reflect.Type
	// Raw is a hand-written JSON Schema used when no Go type exists (for
	// example handler-local anonymous decode structs that are awkward to
	// name). Mutually exclusive with Type.
	Raw jsonSchema
	// Name overrides the generated schema component name. Only used with
	// Type; defaults to "<pkg>.<TypeName>".
	Name string
}

// Ref returns a SchemaRef that reflects the given Go type.
func Ref(t reflect.Type) SchemaRef {
	return SchemaRef{Type: t}
}

// RefOf is a convenience wrapper around Ref for zero-value struct literals.
func RefOf(v any) SchemaRef {
	return Ref(reflect.TypeOf(v))
}

// Raw returns a SchemaRef carrying a hand-written JSON Schema registered
// under the given component name.
func Raw(name string, schema jsonSchema) SchemaRef {
	return SchemaRef{Raw: schema, Name: name}
}

// String returns a SchemaRef for a scalar JSON Schema.
func String() SchemaRef {
	return Raw("", jsonSchema{"type": "string"})
}

// Integer returns a SchemaRef for an integer scalar.
func Integer(format string) SchemaRef {
	s := jsonSchema{"type": "integer"}
	if format != "" {
		s["format"] = format
	}
	return Raw("", s)
}

// Boolean returns a SchemaRef for a boolean scalar.
func Boolean() SchemaRef {
	return Raw("", jsonSchema{"type": "boolean"})
}

// StringArray returns a SchemaRef for an array of strings.
func StringArray() SchemaRef {
	return Raw("", jsonSchema{"type": "array", "items": jsonSchema{"type": "string"}})
}

// typeRegistry accumulates component schemas while reflecting.
type typeRegistry struct {
	schemas  map[string]jsonSchema
	visiting map[reflect.Type]bool
}

func newTypeRegistry() *typeRegistry {
	return &typeRegistry{
		schemas:  make(map[string]jsonSchema),
		visiting: make(map[reflect.Type]bool),
	}
}

// schemaName derives a stable, collision-free component name for a named
// type: "<pkg>.<TypeName>" where pkg is the last path segment of the type's
// package path. Unnamed types return "" and are inlined.
func schemaName(t reflect.Type) string {
	if t.Name() == "" {
		return ""
	}
	if t.PkgPath() == "" {
		// Predeclared types (string, int64, ...) are never registered.
		return ""
	}
	pkg := t.PkgPath()
	if idx := strings.LastIndex(pkg, "/"); idx >= 0 {
		pkg = pkg[idx+1:]
	}
	return pkg + "." + t.Name()
}

// reflectSchema converts t into a JSON Schema, registering named types into
// the registry and returning a $ref for them.
func (r *typeRegistry) reflectSchema(t reflect.Type) (jsonSchema, error) {
	switch t.Kind() {
	case reflect.Pointer:
		return r.reflectSchema(t.Elem())
	case reflect.Interface:
		return jsonSchema{}, nil // any
	case reflect.Slice, reflect.Array:
		if t == reflect.TypeOf(json.RawMessage{}) {
			// RawMessage is arbitrary JSON.
			return jsonSchema{}, nil
		}
		if t.Elem().Kind() == reflect.Uint8 {
			// []byte encodes as base64 string.
			return jsonSchema{"type": "string", "format": "byte"}, nil
		}
		items, err := r.reflectSchema(t.Elem())
		if err != nil {
			return nil, err
		}
		return jsonSchema{"type": "array", "items": items}, nil
	case reflect.Map:
		values, err := r.reflectSchema(t.Elem())
		if err != nil {
			return nil, err
		}
		return jsonSchema{"type": "object", "additionalProperties": values}, nil
	case reflect.Struct:
		if t == reflect.TypeOf(time.Time{}) {
			return jsonSchema{"type": "string", "format": "date-time"}, nil
		}
		if name := schemaName(t); name != "" {
			// Named struct: register as a component and return $ref.
			if _, ok := r.schemas[name]; ok {
				return jsonSchema{"$ref": "#/components/schemas/" + name}, nil
			}
			if r.visiting[t] {
				// Cycle: register a placeholder that the enclosing
				// registration will fill in.
				r.schemas[name] = jsonSchema{"type": "object"}
				return jsonSchema{"$ref": "#/components/schemas/" + name}, nil
			}
			r.visiting[t] = true
			s, err := r.structSchema(t)
			if err != nil {
				return nil, err
			}
			r.schemas[name] = s
			delete(r.visiting, t)
			return jsonSchema{"$ref": "#/components/schemas/" + name}, nil
		}
		return r.structSchema(t)
	case reflect.String:
		return jsonSchema{"type": "string"}, nil
	case reflect.Bool:
		return jsonSchema{"type": "boolean"}, nil
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
		reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
		return jsonSchema{"type": "integer", "format": "int64"}, nil
	case reflect.Float32, reflect.Float64:
		return jsonSchema{"type": "number", "format": "double"}, nil
	default:
		return jsonSchema{}, nil
	}
}

// structSchema builds an object schema from a struct, honoring encoding/json
// semantics: json tags, omitempty (optional), "-" skip, and inline expansion
// of anonymous embedded structs.
func (r *typeRegistry) structSchema(t reflect.Type) (jsonSchema, error) {
	props := make(map[string]any)
	required := []string{}
	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)
		if field.PkgPath != "" && !field.Anonymous {
			// Unexported non-embedded field: ignored by encoding/json.
			continue
		}
		tag := field.Tag.Get("json")
		if tag == "-" {
			continue
		}
		if field.Anonymous && tag == "" {
			// Inline embedded struct (encoding/json flattening).
			if field.Type.Kind() == reflect.Pointer {
				field.Type = field.Type.Elem()
			}
			if field.Type.Kind() != reflect.Struct {
				continue
			}
			embedded, err := r.structSchema(field.Type)
			if err != nil {
				return nil, err
			}
			for name, prop := range embedded["properties"].(map[string]any) {
				props[name] = prop
			}
			if embeddedReq, ok := embedded["required"].([]string); ok {
				required = append(required, embeddedReq...)
			}
			continue
		}
		name := field.Name
		optional := false
		if tag != "" {
			if idx := strings.Index(tag, ","); idx >= 0 {
				name = tag[:idx]
				optional = optional || strings.Contains(tag[idx:], "omitempty")
			} else {
				name = tag
			}
		}
		fieldType := field.Type
		if fieldType.Kind() == reflect.Pointer {
			// Pointers never render a required field.
			optional = true
		}
		prop, err := r.reflectSchema(fieldType)
		if err != nil {
			return nil, err
		}
		props[name] = prop
		if !optional {
			required = append(required, name)
		}
	}
	s := jsonSchema{"type": "object", "properties": props}
	if len(required) > 0 {
		s["required"] = required
	}
	return s, nil
}

// schemaRef resolves a SchemaRef into a JSON Schema and registers any named
// types into the document's components. Raw refs with a Name are registered
// as components too, so hand-written schemas referenced from multiple places
// (e.g. ErrorResponse) are emitted once and $ref'd everywhere else.
func (r *typeRegistry) resolve(ref SchemaRef) (jsonSchema, error) {
	if ref.Type != nil {
		if ref.Name != "" {
			// Force a custom component name.
			if ref.Type.Kind() != reflect.Struct {
				return nil, fmt.Errorf("schema %q: forced name requires a struct type, got %s", ref.Name, ref.Type)
			}
			s, err := r.structSchema(ref.Type)
			if err != nil {
				return nil, err
			}
			r.schemas[ref.Name] = s
			return jsonSchema{"$ref": "#/components/schemas/" + ref.Name}, nil
		}
		return r.reflectSchema(ref.Type)
	}
	if ref.Raw != nil {
		if ref.Name != "" {
			r.schemas[ref.Name] = ref.Raw
			return jsonSchema{"$ref": "#/components/schemas/" + ref.Name}, nil
		}
		return ref.Raw, nil
	}
	return nil, fmt.Errorf("empty SchemaRef: neither Type nor Raw set")
}

// checkRefs validates that every $ref in the registered schemas resolves.
func (r *typeRegistry) checkRefs() error {
	for name, schema := range r.schemas {
		if err := checkSchemaRefs(name, schema, r.schemas); err != nil {
			return err
		}
	}
	return nil
}

// checkDocumentRefs validates every $ref in a fully assembled document,
// including unnamed Raw schemas inlined into paths (union wrappers, list
// envelopes) that never become components.
func checkDocumentRefs(context string, doc map[string]any, schemas map[string]jsonSchema) error {
	return checkSchemaRefs(context, doc, schemas)
}

func checkSchemaRefs(context string, schema any, schemas map[string]jsonSchema) error {
	var walk func(any) error
	walk = func(v any) error {
		switch node := v.(type) {
		case jsonSchema:
			return walk(map[string]any(node))
		case map[string]jsonSchema:
			for _, child := range node {
				if err := walk(child); err != nil {
					return err
				}
			}
			return nil
		case map[string]any:
			if ref, ok := node["$ref"].(string); ok {
				name := strings.TrimPrefix(ref, "#/components/schemas/")
				if _, exists := schemas[name]; !exists {
					return fmt.Errorf("schema %s: unresolved $ref %q", context, ref)
				}
			}
			for _, child := range node {
				if err := walk(child); err != nil {
					return err
				}
			}
		case []any:
			for _, child := range node {
				if err := walk(child); err != nil {
					return err
				}
			}
		}
		return nil
	}
	return walk(schema)
}
