package openapi

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

	"github.com/bitechdev/ResolveSpec/pkg/modelregistry"
	"github.com/bitechdev/ResolveSpec/pkg/spectypes"
)

// OpenAPISpec represents the OpenAPI 3.0 specification structure
type OpenAPISpec struct {
	OpenAPI    string                `json:"openapi"`
	Info       Info                  `json:"info"`
	Servers    []Server              `json:"servers,omitempty"`
	Paths      map[string]PathItem   `json:"paths"`
	Components Components            `json:"components"`
	Security   []map[string][]string `json:"security,omitempty"`
}

type Info struct {
	Title       string   `json:"title"`
	Description string   `json:"description,omitempty"`
	Version     string   `json:"version"`
	Contact     *Contact `json:"contact,omitempty"`
}

type Contact struct {
	Name  string `json:"name,omitempty"`
	URL   string `json:"url,omitempty"`
	Email string `json:"email,omitempty"`
}

type Server struct {
	URL         string `json:"url"`
	Description string `json:"description,omitempty"`
}

type PathItem struct {
	Get     *Operation `json:"get,omitempty"`
	Post    *Operation `json:"post,omitempty"`
	Put     *Operation `json:"put,omitempty"`
	Patch   *Operation `json:"patch,omitempty"`
	Delete  *Operation `json:"delete,omitempty"`
	Options *Operation `json:"options,omitempty"`
}

type Operation struct {
	Summary     string                `json:"summary,omitempty"`
	Description string                `json:"description,omitempty"`
	OperationID string                `json:"operationId,omitempty"`
	Tags        []string              `json:"tags,omitempty"`
	Parameters  []Parameter           `json:"parameters,omitempty"`
	RequestBody *RequestBody          `json:"requestBody,omitempty"`
	Responses   map[string]Response   `json:"responses"`
	Security    []map[string][]string `json:"security,omitempty"`
}

type Parameter struct {
	Name        string      `json:"name"`
	In          string      `json:"in"` // "query", "header", "path", "cookie"
	Description string      `json:"description,omitempty"`
	Required    bool        `json:"required,omitempty"`
	Schema      *Schema     `json:"schema,omitempty"`
	Example     interface{} `json:"example,omitempty"`
}

type RequestBody struct {
	Description string               `json:"description,omitempty"`
	Required    bool                 `json:"required,omitempty"`
	Content     map[string]MediaType `json:"content"`
}

type MediaType struct {
	Schema  *Schema     `json:"schema,omitempty"`
	Example interface{} `json:"example,omitempty"`
}

type Response struct {
	Description string               `json:"description"`
	Content     map[string]MediaType `json:"content,omitempty"`
}

type Components struct {
	Schemas         map[string]Schema         `json:"schemas,omitempty"`
	SecuritySchemes map[string]SecurityScheme `json:"securitySchemes,omitempty"`
}

type Schema struct {
	Type                 string             `json:"type,omitempty"`
	Format               string             `json:"format,omitempty"`
	Description          string             `json:"description,omitempty"`
	Properties           map[string]*Schema `json:"properties,omitempty"`
	Items                *Schema            `json:"items,omitempty"`
	Required             []string           `json:"required,omitempty"`
	Ref                  string             `json:"$ref,omitempty"`
	Enum                 []interface{}      `json:"enum,omitempty"`
	Example              interface{}        `json:"example,omitempty"`
	AdditionalProperties interface{}        `json:"additionalProperties,omitempty"`
	OneOf                []*Schema          `json:"oneOf,omitempty"`
	AnyOf                []*Schema          `json:"anyOf,omitempty"`
}

type SecurityScheme struct {
	Type         string `json:"type"` // "apiKey", "http", "oauth2", "openIdConnect"
	Description  string `json:"description,omitempty"`
	Name         string `json:"name,omitempty"`         // For apiKey
	In           string `json:"in,omitempty"`           // For apiKey: "query", "header", "cookie"
	Scheme       string `json:"scheme,omitempty"`       // For http: "basic", "bearer"
	BearerFormat string `json:"bearerFormat,omitempty"` // For http bearer
}

// GeneratorConfig holds configuration for OpenAPI spec generation
type GeneratorConfig struct {
	Title               string
	Description         string
	Version             string
	BaseURL             string
	Registry            *modelregistry.DefaultModelRegistry
	IncludeRestheadSpec bool
	IncludeResolveSpec  bool
	IncludeFuncSpec     bool
	FuncSpecEndpoints   map[string]FuncSpecEndpoint // path -> endpoint info
}

// FuncSpecEndpoint represents a FuncSpec endpoint for OpenAPI generation
type FuncSpecEndpoint struct {
	Path        string
	Method      string
	Summary     string
	Description string
	SQLQuery    string
	Parameters  []string // Parameter names extracted from SQL
}

// Generator creates OpenAPI specifications
type Generator struct {
	config GeneratorConfig
}

// NewGenerator creates a new OpenAPI generator
func NewGenerator(config GeneratorConfig) *Generator {
	if config.Title == "" {
		config.Title = "ResolveSpec API"
	}
	if config.Version == "" {
		config.Version = "1.0.0"
	}
	return &Generator{config: config}
}

// Generate creates the complete OpenAPI specification
func (g *Generator) Generate() (*OpenAPISpec, error) {
	spec := &OpenAPISpec{
		OpenAPI: "3.0.0",
		Info: Info{
			Title:       g.config.Title,
			Description: g.config.Description,
			Version:     g.config.Version,
		},
		Paths: make(map[string]PathItem),
		Components: Components{
			Schemas:         make(map[string]Schema),
			SecuritySchemes: g.generateSecuritySchemes(),
		},
	}

	if g.config.BaseURL != "" {
		spec.Servers = []Server{
			{URL: g.config.BaseURL, Description: "API Server"},
		}
	}

	// Add common schemas
	g.addCommonSchemas(spec)

	// Generate paths and schemas from registered models
	if err := g.generateFromModels(spec); err != nil {
		return nil, err
	}

	return spec, nil
}

// GenerateJSON generates OpenAPI spec as JSON string
func (g *Generator) GenerateJSON() (string, error) {
	spec, err := g.Generate()
	if err != nil {
		return "", err
	}

	data, err := json.MarshalIndent(spec, "", "  ")
	if err != nil {
		return "", fmt.Errorf("failed to marshal spec: %w", err)
	}

	return string(data), nil
}

// generateSecuritySchemes creates security scheme definitions
func (g *Generator) generateSecuritySchemes() map[string]SecurityScheme {
	return map[string]SecurityScheme{
		"BearerAuth": {
			Type:         "http",
			Scheme:       "bearer",
			BearerFormat: "JWT",
			Description:  "JWT Bearer token authentication",
		},
		"SessionToken": {
			Type:        "apiKey",
			In:          "header",
			Name:        "Authorization",
			Description: "Session token authentication",
		},
		"CookieAuth": {
			Type:        "apiKey",
			In:          "cookie",
			Name:        "session_token",
			Description: "Cookie-based session authentication",
		},
		"HeaderAuth": {
			Type:        "apiKey",
			In:          "header",
			Name:        "X-User-ID",
			Description: "Header-based user authentication",
		},
	}
}

// addCommonSchemas adds common reusable schemas
func (g *Generator) addCommonSchemas(spec *OpenAPISpec) {
	// Response wrapper schema
	spec.Components.Schemas["Response"] = Schema{
		Type: "object",
		Properties: map[string]*Schema{
			"success":  {Type: "boolean", Description: "Indicates if the operation was successful"},
			"data":     {Description: "The response data"},
			"metadata": {Ref: "#/components/schemas/Metadata"},
			"error":    {Ref: "#/components/schemas/APIError"},
		},
	}

	// Metadata schema
	spec.Components.Schemas["Metadata"] = Schema{
		Type: "object",
		Properties: map[string]*Schema{
			"total":     {Type: "integer", Description: "Total number of records"},
			"count":     {Type: "integer", Description: "Number of records in this response"},
			"filtered":  {Type: "integer", Description: "Number of records after filtering"},
			"limit":     {Type: "integer", Description: "Limit applied"},
			"offset":    {Type: "integer", Description: "Offset applied"},
			"rowNumber": {Type: "integer", Description: "Row number for cursor pagination"},
		},
	}

	// APIError schema
	spec.Components.Schemas["APIError"] = Schema{
		Type: "object",
		Properties: map[string]*Schema{
			"code":    {Type: "string", Description: "Error code"},
			"message": {Type: "string", Description: "Error message"},
			"details": {Type: "string", Description: "Detailed error information"},
		},
	}

	// RequestOptions schema
	spec.Components.Schemas["RequestOptions"] = Schema{
		Type: "object",
		Properties: map[string]*Schema{
			"preload": {
				Type:        "array",
				Description: "Relations to eager load",
				Items:       &Schema{Ref: "#/components/schemas/PreloadOption"},
			},
			"columns": {
				Type:        "array",
				Description: "Columns to select",
				Items:       &Schema{Type: "string"},
			},
			"omitColumns": {
				Type:        "array",
				Description: "Columns to exclude",
				Items:       &Schema{Type: "string"},
			},
			"filters": {
				Type:        "array",
				Description: "Filter conditions",
				Items:       &Schema{Ref: "#/components/schemas/FilterOption"},
			},
			"sort": {
				Type:        "array",
				Description: "Sort specifications",
				Items:       &Schema{Ref: "#/components/schemas/SortOption"},
			},
			"limit":  {Type: "integer", Description: "Maximum number of records"},
			"offset": {Type: "integer", Description: "Number of records to skip"},
		},
	}

	// FilterOption schema
	spec.Components.Schemas["FilterOption"] = Schema{
		Type: "object",
		Properties: map[string]*Schema{
			"column":        {Type: "string", Description: "Column name"},
			"operator":      {Type: "string", Description: "Comparison operator", Enum: []interface{}{"eq", "neq", "gt", "lt", "gte", "lte", "like", "ilike", "in", "not_in", "between", "is_null", "is_not_null"}},
			"value":         {Description: "Filter value"},
			"logicOperator": {Type: "string", Description: "Logic operator", Enum: []interface{}{"AND", "OR"}},
		},
	}

	// SortOption schema
	spec.Components.Schemas["SortOption"] = Schema{
		Type: "object",
		Properties: map[string]*Schema{
			"column":    {Type: "string", Description: "Column name"},
			"direction": {Type: "string", Description: "Sort direction", Enum: []interface{}{"asc", "desc"}},
		},
	}

	// PreloadOption schema
	spec.Components.Schemas["PreloadOption"] = Schema{
		Type: "object",
		Properties: map[string]*Schema{
			"relation": {Type: "string", Description: "Relation name"},
			"columns": {
				Type:        "array",
				Description: "Columns to select from related table",
				Items:       &Schema{Type: "string"},
			},
		},
	}

	// ResolveSpec RequestBody schema
	spec.Components.Schemas["ResolveSpecRequest"] = Schema{
		Type: "object",
		Properties: map[string]*Schema{
			"operation": {Type: "string", Description: "Operation type", Enum: []interface{}{"read", "create", "update", "delete", "meta"}},
			"data":      {Description: "Payload data (object or array)"},
			"id":        {Type: "integer", Description: "Record ID for single operations"},
			"options":   {Ref: "#/components/schemas/RequestOptions"},
		},
	}
}

// generateFromModels generates paths and schemas from registered models
func (g *Generator) generateFromModels(spec *OpenAPISpec) error {
	if g.config.Registry == nil {
		return fmt.Errorf("model registry is required")
	}

	models := g.config.Registry.GetAllModels()

	for name, model := range models {
		// Parse schema.entity from model name
		schema, entity := parseModelName(name)

		// Generate schema for this model
		modelSchema := g.generateModelSchema(model)
		schemaName := formatSchemaName(schema, entity)
		spec.Components.Schemas[schemaName] = modelSchema

		// Generate paths for different frameworks
		if g.config.IncludeRestheadSpec {
			g.generateRestheadSpecPaths(spec, schema, entity, schemaName)
		}

		if g.config.IncludeResolveSpec {
			g.generateResolveSpecPaths(spec, schema, entity, schemaName)
		}
	}

	// Generate FuncSpec paths if configured
	if g.config.IncludeFuncSpec && len(g.config.FuncSpecEndpoints) > 0 {
		g.generateFuncSpecPaths(spec)
	}

	return nil
}

// generateModelSchema creates an OpenAPI schema from a Go struct
func (g *Generator) generateModelSchema(model interface{}) Schema {
	schema := Schema{
		Type:       "object",
		Properties: make(map[string]*Schema),
		Required:   []string{},
	}

	modelType := reflect.TypeOf(model)
	if modelType.Kind() == reflect.Pointer {
		modelType = modelType.Elem()
	}
	if modelType.Kind() != reflect.Struct {
		return schema
	}

	for i := 0; i < modelType.NumField(); i++ {
		field := modelType.Field(i)

		// Skip unexported fields
		if !field.IsExported() {
			continue
		}

		// Get JSON tag name
		jsonTag := field.Tag.Get("json")
		if jsonTag == "-" {
			continue
		}

		fieldName := strings.Split(jsonTag, ",")[0]
		if fieldName == "" {
			fieldName = field.Name
		}

		// Generate property schema
		propSchema := g.generatePropertySchema(field)
		schema.Properties[fieldName] = propSchema

		// Check if field is required (not a pointer and no omitempty)
		if field.Type.Kind() != reflect.Pointer && !strings.Contains(jsonTag, "omitempty") {
			schema.Required = append(schema.Required, fieldName)
		}
	}

	return schema
}

// generatePropertySchema creates a schema for a struct field
func (g *Generator) generatePropertySchema(field reflect.StructField) *Schema {
	schema := &Schema{}

	fieldType := field.Type
	if fieldType.Kind() == reflect.Pointer {
		fieldType = fieldType.Elem()
	}

	// Get description from tag
	if desc := field.Tag.Get("description"); desc != "" {
		schema.Description = desc
	}

	// spectypes PostGIS / pgvector wrappers get dedicated schemas.
	if n, ok := spectypes.SQLTypeName(field.Type); ok {
		switch n {
		case "geometry", "geography":
			schema.Type = "object"
			schema.Format = "geojson"
			return schema
		case "vector", "halfvec":
			schema.Type = "array"
			schema.Items = &Schema{Type: "number"}
			schema.Format = "vector"
			return schema
		case "sparsevec":
			schema.Type = "object"
			schema.Format = "sparsevec"
			return schema
		case "bit":
			schema.Type = "string"
			return schema
		}
	}

	switch fieldType.Kind() {
	case reflect.String:
		schema.Type = "string"
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
		reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
		schema.Type = "integer"
	case reflect.Float32, reflect.Float64:
		schema.Type = "number"
	case reflect.Bool:
		schema.Type = "boolean"
	case reflect.Slice, reflect.Array:
		schema.Type = "array"
		elemType := fieldType.Elem()
		if elemType.Kind() == reflect.Pointer {
			elemType = elemType.Elem()
		}
		if elemType.Kind() == reflect.Struct {
			// Complex type - would need recursive handling
			schema.Items = &Schema{Type: "object"}
		} else {
			schema.Items = g.generatePropertySchema(reflect.StructField{Type: elemType})
		}
	case reflect.Struct:
		// Check for time.Time
		if fieldType.String() == "time.Time" {
			schema.Type = "string"
			schema.Format = "date-time"
		} else {
			schema.Type = "object"
		}
	default:
		schema.Type = "string"
	}

	// Check for custom format from gorm/bun tags
	if gormTag := field.Tag.Get("gorm"); gormTag != "" {
		if strings.Contains(gormTag, "type:uuid") {
			schema.Format = "uuid"
		}
	}

	return schema
}

// parseModelName splits "schema.entity" or returns "public" and entity
func parseModelName(name string) (schema, entity string) {
	parts := strings.Split(name, ".")
	if len(parts) == 2 {
		return parts[0], parts[1]
	}
	return "public", name
}

// formatSchemaName creates a component schema name
func formatSchemaName(schema, entity string) string {
	if schema == "public" {
		return toTitleCase(entity)
	}
	return toTitleCase(schema) + toTitleCase(entity)
}

// toTitleCase converts a string to title case (first letter uppercase)
func toTitleCase(s string) string {
	if s == "" {
		return ""
	}
	if len(s) == 1 {
		return strings.ToUpper(s)
	}
	return strings.ToUpper(s[:1]) + s[1:]
}
