package swaggerator

import (
	"context"
	"embed"
	"fmt"
	"io/fs"
	"net/http"
	"path"
	"strings"

	"github.com/centralmind/gateway/connectors"
	"github.com/sirupsen/logrus"
	"golang.org/x/xerrors"

	"github.com/centralmind/gateway/model"
	"github.com/centralmind/gateway/plugins"
	"github.com/danielgtaylor/huma/v2"
)

//go:embed dist
var swagfs embed.FS

// Schema dynamically generates an OpenAPI 3.1 schema based on the given table schema.
func Schema(schema model.Config, prefix string, addresses ...string) (*huma.OpenAPI, error) {
	api := huma.DefaultConfig(schema.API.Name, "3.1.0").OpenAPI
	api.Info.Title = schema.API.Name
	api.Info.Description = "Config that dynamically generates accessor for data"
	api.Info.Version = schema.API.Version

	connector, err := connectors.New(schema.Database.Type, schema.Database.Connection)
	if err != nil {
		return nil, xerrors.Errorf("unable to init connector: %w", err)
	}

	// Add all server addresses
	for i, address := range addresses {
		var description string
		switch {
		case i == 0 && strings.HasPrefix(address, "http://localhost"):
			description = "Local development server"
		case strings.Contains(address, "dev"):
			description = "Development server"
		case strings.Contains(address, "stage"):
			description = "Staging server"
		case strings.Contains(address, "prod"):
			description = "Production server"
		default:
			description = "Server " + address
		}
		api.Servers = append(api.Servers, &huma.Server{
			URL:         address,
			Description: description,
		})
	}

	// If no servers were provided, add a default one
	if len(api.Servers) == 0 {
		api.Servers = append(api.Servers, &huma.Server{
			URL:         "http://localhost:9090",
			Description: "localhost",
		})
	}

	// Iterate through tables and generate OpenAPI schemas
	allEndpoints := schema.Database.GetAllEndpoints()
	for _, endpoint := range allEndpoints {
		cols, err := connector.InferQuery(context.Background(), endpoint.Query)
		if err != nil {
			logrus.Warnf("unable to infer query %s: %v", endpoint.Query, err)
		}
		schemaProps := map[string]*huma.Schema{}
		for _, col := range cols {
			schemaProps[col.Name] = &huma.Schema{
				Type: string(col.Type),
			}
			if col.Type == model.TypeDatetime {
				schemaProps[col.Name] = &huma.Schema{
					Type:   "string",
					Format: "date-time",
				}
			}
		}

		var params []*huma.Param
		bodyProps := map[string]*huma.Schema{}
		for _, param := range endpoint.Params {
			if param.Location == "" {
				param.Location = "query"
			}
			if param.Location == "body" {
				bodyProps[param.Name] = &huma.Schema{
					Type:    param.Type,
					Format:  param.Format,
					Default: param.Default,
				}
				continue
			}
			params = append(params, &huma.Param{
				Name:     param.Name,
				In:       param.Location,
				Required: param.Required,
				Schema: &huma.Schema{
					Type:    param.Type,
					Format:  param.Format,
					Default: param.Default,
				},
			})
		}
		if len(bodyProps) > 0 {
			params = append(params, &huma.Param{
				Name:     "body",
				In:       "body",
				Required: true,
				Schema: &huma.Schema{
					Type:       "object",
					Properties: bodyProps,
				},
			})
		}
		resSchema := &huma.Schema{
			Type:       "object",
			Properties: schemaProps,
		}
		if endpoint.IsArrayResult {
			resSchema = &huma.Schema{
				Type:  "array",
				Items: resSchema,
			}
		}
		operation := &huma.Operation{
			Summary:     endpoint.Summary,
			Description: endpoint.Description,
			OperationID: endpoint.MCPMethod,
			Tags:        []string{endpoint.Group},
			Parameters:  params,
			Responses: map[string]*huma.Response{
				"200": {
					Description: "Success",
					Content: map[string]*huma.MediaType{
						"application/json": {
							Schema: resSchema,
						},
					},
				},
				"404": {
					Description: "Not Found",
					Content: map[string]*huma.MediaType{
						"application/json": {
							Schema: &huma.Schema{
								Type: "object",
								Properties: map[string]*huma.Schema{
									"error": {Type: "string"},
								},
							},
						},
					},
				},
				"500": {
					Description: "Error",
					Content: map[string]*huma.MediaType{
						"application/json": {
							Schema: &huma.Schema{
								Type: "object",
								Properties: map[string]*huma.Schema{
									"error": {Type: "string"},
								},
							},
						},
					},
				},
			},
		}
		httpPath := endpoint.HTTPPath
		if prefix != "" {
			httpPath = path.Join("/", prefix, httpPath)
		}
		if api.Paths == nil {
			api.Paths = make(map[string]*huma.PathItem)
		}
		if _, ok := api.Paths[httpPath]; !ok {
			api.Paths[httpPath] = &huma.PathItem{}
		}
		switch endpoint.HTTPMethod {
		case "GET":
			api.Paths[httpPath].Get = operation
		case "DELETE":
			api.Paths[httpPath].Delete = operation
		case "POST":
			api.Paths[httpPath].Post = operation
		case "PATCH":
			api.Paths[httpPath].Patch = operation
		case "PUT":
			api.Paths[httpPath].Put = operation
		}
	}

	api, err = plugins.Enrich(schema.Plugins, api)
	if err != nil {
		return nil, xerrors.Errorf("unable to enrich swagger schema: %w", err)
	}
	return api, nil
}

// AddRawEndpoints adds Raw API endpoints to an existing OpenAPI schema
func AddRawEndpoints(api *huma.OpenAPI, schema model.Config, prefix string) (*huma.OpenAPI, error) {
	// Define Raw API endpoints
	rawPath := "/raw"
	if prefix != "" {
		rawPath = path.Join("/", prefix, "raw")
	}

	// List Tables endpoint
	listTablesOperation := &huma.Operation{
		Summary:     "List available tables",
		Description: fmt.Sprintf("Return list of tables that available for data in %s database", schema.Database.Type),
		OperationID: "list_tables",
		Tags:        []string{"Raw"},
		Responses: map[string]*huma.Response{
			"200": {
				Description: "Success",
				Content: map[string]*huma.MediaType{
					"application/json": {
						Schema: &huma.Schema{
							Type: "array",
							Items: &huma.Schema{
								Type: "object",
								Properties: map[string]*huma.Schema{
									"name": {Type: "string"},
									"columns": {
										Type: "array",
										Items: &huma.Schema{
											Type: "object",
											Properties: map[string]*huma.Schema{
												"name": {Type: "string"},
												"type": {Type: "string"},
											},
										},
									},
									"row_count": {Type: "integer"},
								},
							},
						},
					},
				},
			},
			"500": {
				Description: "Error",
				Content: map[string]*huma.MediaType{
					"application/json": {
						Schema: &huma.Schema{
							Type: "object",
							Properties: map[string]*huma.Schema{
								"error": {Type: "string"},
							},
						},
					},
				},
			},
		},
	}

	// Discover Data endpoint
	discoverDataOperation := &huma.Operation{
		Summary:     "Discover data structure",
		Description: fmt.Sprintf("Discover data structure for connected %s gateway", schema.Database.Type),
		OperationID: "discover_data",
		Tags:        []string{"Raw"},
		Parameters: []*huma.Param{
			{
				Name:     "tables_list",
				In:       "query",
				Required: false,
				Schema: &huma.Schema{
					Type:        "string",
					Description: "Comma separated table names to fetch data samples",
				},
			},
		},
		Responses: map[string]*huma.Response{
			"200": {
				Description: "Success",
				Content: map[string]*huma.MediaType{
					"application/json": {
						Schema: &huma.Schema{
							Type: "array",
							Items: &huma.Schema{
								Type: "object",
								Properties: map[string]*huma.Schema{
									"name": {Type: "string"},
									"columns": {
										Type: "array",
										Items: &huma.Schema{
											Type: "object",
											Properties: map[string]*huma.Schema{
												"name": {Type: "string"},
												"type": {Type: "string"},
											},
										},
									},
									"sample": {
										Type: "array",
										Items: &huma.Schema{
											Type: "object",
										},
									},
									"row_count": {Type: "integer"},
								},
							},
						},
					},
				},
			},
			"500": {
				Description: "Error",
				Content: map[string]*huma.MediaType{
					"application/json": {
						Schema: &huma.Schema{
							Type: "object",
							Properties: map[string]*huma.Schema{
								"error": {Type: "string"},
							},
						},
					},
				},
			},
		},
	}

	// Prepare Query endpoint
	prepareQueryOperation := &huma.Operation{
		Summary:     "Verify and prepare query",
		Description: fmt.Sprintf("Verify query and prepare output structure for query in %s database", schema.Database.Type),
		OperationID: "prepare_query",
		Tags:        []string{"Raw"},
		Parameters: []*huma.Param{
			{
				Name:     "query",
				In:       "query",
				Required: true,
				Schema: &huma.Schema{
					Type:        "string",
					Description: "SQL query to verify",
				},
			},
		},
		Responses: map[string]*huma.Response{
			"200": {
				Description: "Success",
				Content: map[string]*huma.MediaType{
					"application/json": {
						Schema: &huma.Schema{
							Type: "array",
							Items: &huma.Schema{
								Type: "object",
								Properties: map[string]*huma.Schema{
									"name": {Type: "string"},
									"type": {Type: "string"},
								},
							},
						},
					},
				},
			},
			"500": {
				Description: "Error",
				Content: map[string]*huma.MediaType{
					"application/json": {
						Schema: &huma.Schema{
							Type: "object",
							Properties: map[string]*huma.Schema{
								"error": {Type: "string"},
							},
						},
					},
				},
			},
		},
	}

	// Query endpoint
	queryOperation := &huma.Operation{
		Summary:     "Execute query",
		Description: fmt.Sprintf("Query data structure for connected %s gateway", schema.Database.Type),
		OperationID: "query",
		Tags:        []string{"Raw"},
		Parameters: []*huma.Param{
			{
				Name:     "query",
				In:       "query",
				Required: true,
				Schema: &huma.Schema{
					Type:        "string",
					Description: "SQL query to execute",
				},
			},
		},
		Responses: map[string]*huma.Response{
			"200": {
				Description: "Success",
				Content: map[string]*huma.MediaType{
					"application/json": {
						Schema: &huma.Schema{
							Type: "array",
							Items: &huma.Schema{
								Type: "object",
							},
						},
					},
				},
			},
			"500": {
				Description: "Error",
				Content: map[string]*huma.MediaType{
					"application/json": {
						Schema: &huma.Schema{
							Type: "object",
							Properties: map[string]*huma.Schema{
								"error": {Type: "string"},
							},
						},
					},
				},
			},
		},
	}

	// Add operations to paths
	if api.Paths == nil {
		api.Paths = make(map[string]*huma.PathItem)
	}

	api.Paths[path.Join(rawPath, "list_tables")] = &huma.PathItem{
		Get: listTablesOperation,
	}
	api.Paths[path.Join(rawPath, "discover_data")] = &huma.PathItem{
		Get: discoverDataOperation,
	}
	api.Paths[path.Join(rawPath, "prepare_query")] = &huma.PathItem{
		Get: prepareQueryOperation,
	}
	api.Paths[path.Join(rawPath, "query")] = &huma.PathItem{
		Get: queryOperation,
	}

	return api, nil
}

func byteHandler(b []byte) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		w.Write(b)
	}
}

func RegisterRoute(mux *http.ServeMux, prefix string, spec []byte) {
	// render the index template with the proper spec name inserted
	static, err := fs.Sub(swagfs, "dist")
	if err != nil {
		logrus.Errorf("Failed to access embedded swagger files: %v", err)
		return
	}

	// Handle empty prefix properly
	var swaggerPath string
	if prefix == "" {
		swaggerPath = "/swagger"
	} else {
		swaggerPath = path.Join("/", prefix, "swagger")
	}

	apiJsonPath := path.Join(swaggerPath, "open_api.json")

	// Register the API JSON endpoint
	mux.HandleFunc(apiJsonPath, byteHandler(spec))

	// Create a simple handler that redirects /swagger to /swagger/
	rootHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == swaggerPath {
			http.Redirect(w, r, swaggerPath+"/", http.StatusMovedPermanently)
			return
		}
		http.StripPrefix(swaggerPath, http.FileServer(http.FS(static))).ServeHTTP(w, r)
	})

	// Register handlers
	mux.Handle(swaggerPath, rootHandler)
	mux.Handle(swaggerPath+"/", http.StripPrefix(swaggerPath, http.FileServer(http.FS(static))))
}
