package signoz

import (
	"bytes"
	"context"
	"encoding/json"
	"os"
	"reflect"

	"github.com/SigNoz/signoz/pkg/alertmanager"
	"github.com/SigNoz/signoz/pkg/apiserver"
	"github.com/SigNoz/signoz/pkg/apiserver/signozapiserver"
	"github.com/SigNoz/signoz/pkg/authz"
	"github.com/SigNoz/signoz/pkg/factory"
	"github.com/SigNoz/signoz/pkg/flagger"
	"github.com/SigNoz/signoz/pkg/gateway"
	"github.com/SigNoz/signoz/pkg/global"
	"github.com/SigNoz/signoz/pkg/http/handler"
	"github.com/SigNoz/signoz/pkg/instrumentation"
	"github.com/SigNoz/signoz/pkg/modules/authdomain"
	"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
	"github.com/SigNoz/signoz/pkg/modules/dashboard"
	"github.com/SigNoz/signoz/pkg/modules/fields"
	"github.com/SigNoz/signoz/pkg/modules/inframonitoring"
	"github.com/SigNoz/signoz/pkg/modules/llmpricingrule"
	"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
	"github.com/SigNoz/signoz/pkg/modules/organization"
	"github.com/SigNoz/signoz/pkg/modules/preference"
	"github.com/SigNoz/signoz/pkg/modules/promote"
	"github.com/SigNoz/signoz/pkg/modules/rawdataexport"
	"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
	"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
	"github.com/SigNoz/signoz/pkg/modules/session"
	"github.com/SigNoz/signoz/pkg/modules/spanmapper"
	"github.com/SigNoz/signoz/pkg/modules/tracedetail"
	"github.com/SigNoz/signoz/pkg/modules/user"
	"github.com/SigNoz/signoz/pkg/querier"
	"github.com/SigNoz/signoz/pkg/ruler"
	"github.com/SigNoz/signoz/pkg/types/authtypes"
	"github.com/SigNoz/signoz/pkg/zeus"
	"github.com/swaggest/jsonschema-go"
	"github.com/swaggest/openapi-go"
	"github.com/swaggest/openapi-go/openapi3"
	"gopkg.in/yaml.v2"
)

type OpenAPI struct {
	apiserver apiserver.APIServer
	reflector *openapi3.Reflector
	collector *handler.OpenAPICollector
}

func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumentation) (*OpenAPI, error) {
	apiserver, err := signozapiserver.NewFactory(
		struct{ organization.Getter }{},
		struct{ authz.AuthZ }{},
		struct{ organization.Handler }{},
		struct{ user.Handler }{},
		struct{ session.Handler }{},
		struct{ authdomain.Handler }{},
		struct{ preference.Handler }{},
		struct{ global.Handler }{},
		struct{ promote.Handler }{},
		struct{ flagger.Handler }{},
		struct{ dashboard.Module }{},
		struct{ dashboard.Handler }{},
		struct{ metricsexplorer.Handler }{},
		struct{ inframonitoring.Handler }{},
		struct{ gateway.Handler }{},
		struct{ fields.Handler }{},
		struct{ authz.Handler }{},
		struct{ rawdataexport.Handler }{},
		struct{ zeus.Handler }{},
		struct{ querier.Handler }{},
		struct{ serviceaccount.Handler }{},
		struct{ factory.Handler }{},
		struct{ cloudintegration.Handler }{},
		struct{ rulestatehistory.Handler }{},
		struct{ spanmapper.Handler }{},
		struct{ alertmanager.Handler }{},
		struct{ llmpricingrule.Handler }{},
		struct{ tracedetail.Handler }{},
		struct{ ruler.Handler }{},
	).New(ctx, instrumentation.ToProviderSettings(), apiserver.Config{})
	if err != nil {
		return nil, err
	}

	reflector := openapi3.NewReflector()
	reflector.JSONSchemaReflector().DefaultOptions = append(reflector.JSONSchemaReflector().DefaultOptions, jsonschema.InterceptDefName(func(t reflect.Type, defaultDefName string) string {
		if defaultDefName == "RenderSuccessResponse" {
			field, ok := t.FieldByName("Data")
			if !ok {
				return defaultDefName
			}

			return field.Type.Name()
		}

		return defaultDefName
	}))

	reflector.Spec.WithInfo(*(&openapi3.Info{}).
		WithTitle("SigNoz").
		WithDescription("OpenTelemetry-Native Logs, Metrics and Traces in a single pane").
		WithTermsOfService("https://signoz.io/terms-of-service/").
		WithContact(*(&openapi3.Contact{}).
			WithName("SigNoz Support").
			WithURL("https://signoz.io").
			WithEmail("support@signoz.io")),
	)

	reflector.Spec.WithServers(
		// Default server
		*(&openapi3.Server{}).WithURL("https://{host}:{port}{base_path}").
			WithDescription("The fully qualified URL to the SigNoz APIServer.").
			WithVariablesItem("host", *(&openapi3.ServerVariable{}).
				WithDefault("localhost").
				WithDescription("The host of the SigNoz APIServer")).
			WithVariablesItem("port", *(&openapi3.ServerVariable{}).
				WithDefault("8080").
				WithDescription("The port of the SigNoz APIServer")).
			WithVariablesItem("base_path", *(&openapi3.ServerVariable{}).
				WithDefault("/").
				WithDescription("The base path of the SigNoz APIServer")),
	)

	reflector.SpecSchema().SetAPIKeySecurity(authtypes.IdentNProviderAPIKey.StringValue(), "SigNoz-Api-Key", openapi.InHeader, "API Keys")
	reflector.SpecSchema().SetHTTPBearerTokenSecurity(authtypes.IdentNProviderTokenizer.StringValue(), "Tokenizer", "Tokens generated by the tokenizer")

	collector := handler.NewOpenAPICollector(reflector)

	return &OpenAPI{
		apiserver: apiserver,
		reflector: reflector,
		collector: collector,
	}, nil
}

func (openapi *OpenAPI) CreateAndWrite(path string) error {
	if err := openapi.apiserver.Router().Walk(openapi.collector.Walker); err != nil {
		return err
	}

	// The library's MarshalYAML does a JSON round-trip that converts all numbers
	// to float64, causing large integers (e.g. epoch millisecond timestamps) to
	// render in scientific notation (1.6409952e+12).
	jsonData, err := openapi.reflector.Spec.MarshalJSON()
	if err != nil {
		return err
	}

	dec := json.NewDecoder(bytes.NewReader(jsonData))
	dec.UseNumber()

	var v any
	if err := dec.Decode(&v); err != nil {
		return err
	}

	convertJSONNumbers(v)

	spec, err := yaml.Marshal(v)
	if err != nil {
		return err
	}

	return os.WriteFile(path, spec, 0o600)
}

// convertJSONNumbers recursively walks a decoded JSON structure and converts
// json.Number values to int64 (preferred) or float64 so that YAML marshaling
// renders them as plain numbers instead of quoted strings.
func convertJSONNumbers(v interface{}) {
	switch val := v.(type) {
	case map[string]interface{}:
		for k, elem := range val {
			if n, ok := elem.(json.Number); ok {
				if i, err := n.Int64(); err == nil {
					val[k] = i
				} else if f, err := n.Float64(); err == nil {
					val[k] = f
				}
			} else {
				convertJSONNumbers(elem)
			}
		}
	case []interface{}:
		for i, elem := range val {
			if n, ok := elem.(json.Number); ok {
				if i64, err := n.Int64(); err == nil {
					val[i] = i64
				} else if f, err := n.Float64(); err == nil {
					val[i] = f
				}
			} else {
				convertJSONNumbers(elem)
			}
		}
	}
}
