package service

import (
	"context"
	"dataservices/commons/config"
	"dataservices/commons/grpc"
	"dataservices/commons/oas"
	"dataservices/commons/utils"
	"dataservices/dbhubsdk"
	dto "dataservices/internal/dto/dataapi"
	gatewaydto "dataservices/internal/dto/dataapi_gateway"
	"dataservices/internal/entity"
	"dataservices/internal/repository"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"

	comhttp "dataservices/commons/http"

	"github.com/getkin/kin-openapi/openapi3"
	"github.com/getkin/kin-openapi/openapi3gen"
	"github.com/tidbcloud/account/idl/pbgen/proto/acl"
	"github.com/tidbcloud/dataservices/idl/pbgen/proto/dataapi"
	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"sigs.k8s.io/yaml"
)

func retriveTableMetaColumns(ctx context.Context, projectId, clusterId uint64, cred *dataapi.Credential) ([]*oas.MetaColumn, error) {
	regionName := "us-west-2"
	if config.Global().ServerConf.Env == "prod" {
		cluster, err := grpc.GetClusterWithoutPermCheck(ctx, clusterId)
		if err != nil {
			return nil, err
		}
		regionName = cluster.RegionName
	}
	typ := cred.Type.String()
	key := cred.Key
	accountID := cred.AccountId
	// Temporary method, pretending that all MIXED key are user emails and will be optimized in the future
	if cred.Type == dataapi.CrtType_MIXED {
		res, err := grpc.GetJwtByUser(ctx, &acl.GetJwtByUserReq{
			ClusterId: clusterId,
			Email:     cred.Key,
		})
		if err != nil {
			return nil, err
		}
		typ = dataapi.CrtType_ACCOUNT.String()
		key = dbhubsdk.EncryptDBAccount(res.DbUser, res.Jwt)
	}
	resp, err := comhttp.ExecSql(ctx, regionName, &gatewaydto.ExecSqlReq{
		ProjectId: projectId,
		ClusterId: clusterId,
		Sql:       "select LOWER(TABLE_SCHEMA) as TABLE_SCHEMA, LOWER(TABLE_NAME) as TABLE_NAME, COLUMN_NAME, DATA_TYPE, IS_NULLABLE from information_schema.columns",
		LimitRows: 10000,
		Credential: gatewaydto.Credential{
			Type:      typ,
			Key:       key,
			AccountID: accountID,
		},
	})
	if err != nil {
		return nil, errs.NewInternalGRPC(fmt.Sprintf("get table metadata failed: %s", err), err)
	}
	result := make([]*oas.MetaColumn, 0, len(resp.Data.Rows))
	for _, row := range resp.Data.Rows {
		result = append(result, &oas.MetaColumn{
			TableSchema: row["TABLE_SCHEMA"],
			TableName:   row["TABLE_NAME"],
			ColumnName:  row["COLUMN_NAME"],
			DataType:    row["DATA_TYPE"],
			IsNullable:  row["IS_NULLABLE"],
		})
	}
	return result, nil
}

func oasColumns2ProtoColumns(columns oas.Columns) []*dataapi.Column {
	result := make([]*dataapi.Column, 0, len(columns))
	for _, column := range columns {
		result = append(result, &dataapi.Column{Name: column.ColumnName})
	}
	return result
}

func entityColumns2OasColumns(columns []entity.Column) oas.Columns {
	result := make(oas.Columns, 0, len(columns))
	for _, column := range columns {
		result = append(result, &oas.Column{ColumnName: column.Col})
	}
	return result
}

func buildResponseSchema(columns oas.Columns) *openapi3.SchemaRef {
	schemaRef, _, _ := openapi3gen.NewSchemaRefForValue(&gatewaydto.AdvancedRes{})
	rowsSchema := openapi3.NewObjectSchema()
	// handle column name duplicate
	protoColumns := oasColumns2ProtoColumns(columns)
	entityColumns := gatewaydto.ConvertToExecSqlRespColumns(protoColumns)
	newColumns := entityColumns2OasColumns(entityColumns)
	for _, newColumn := range newColumns {
		rowsSchema.Properties[newColumn.ColumnName] = openapi3.NewSchemaRef("", openapi3.NewStringSchema())
		rowsSchema.Required = append(rowsSchema.Required, newColumn.ColumnName)
	}
	schemaRef.Value.Properties["data"].Value.Properties["rows"].Value.Items = openapi3.NewSchemaRef("", rowsSchema)
	// add required field
	schemaRef.Value.Required = []string{"type", "data"}
	schemaRef.Value.Properties["data"].Value.Required = []string{"columns", "rows", "result"}
	// remove unused properties
	delete(schemaRef.Value.Properties["data"].Value.Properties["result"].Value.Properties, "sql")
	delete(schemaRef.Value.Properties["data"].Value.Properties["result"].Value.Properties, "ai_latency")
	delete(schemaRef.Value.Properties["data"].Value.Properties["result"].Value.Properties, "databases")
	return schemaRef
}

func buildRequestBodySchema(batch bool, args []*entity.EndpointArgs) *openapi3.SchemaRef {
	argsSchema := openapi3.NewObjectSchema()
	for _, arg := range args {
		schema := openapi3.NewStringSchema()
		schema.Description = arg.Description
		schema.Default = arg.Default
		if len(arg.Enum) > 0 {
			for _, item := range strings.Split(arg.Enum, ",") {
				schema.Enum = append(schema.Enum, item)
			}
		}
		argsSchema.Properties[arg.Name] = openapi3.NewSchemaRef("", schema)
		if arg.Required != 0 {
			argsSchema.Required = append(argsSchema.Required, arg.Name)
		}
	}
	if !batch {
		return openapi3.NewSchemaRef("", argsSchema)
	} else {
		result := openapi3.NewObjectSchema()
		item := openapi3.NewArraySchema()
		item.Items = openapi3.NewSchemaRef("", argsSchema)
		result.Properties["items"] = openapi3.NewSchemaRef("", item)
		return openapi3.NewSchemaRef("", result)
	}
}

func firstUpper(s string) string {
	s = strings.ToLower(s)
	if len(s) == 0 {
		return ""
	}
	return strings.ToUpper(s[:1]) + s[1:]
}

func buildEndpointOperationID(endpoint *entity.EndpointPublish) string {
	result := strings.ToLower(endpoint.Method)
	for _, part := range strings.Split(endpoint.Endpoint, "/") {
		part = strings.ReplaceAll(part, "{", "_")
		part = strings.ReplaceAll(part, "}", "_")
		result += firstUpper(part)
	}
	return result
}

func buildEndpointOperationParameters(endpoint *entity.EndpointPublish, args []*entity.EndpointArgs) openapi3.Parameters {
	parameters := make(openapi3.Parameters, 0)
	explode := false
	for _, arg := range args {
		required := true
		if arg.Required == 0 {
			required = false
		}
		argType := arg.Type
		var defaultValue any
		defaultValue = arg.Default
		// fix type mismatch cause OpenAI 500
		if argType == entity.EndpointArgTypeNumber || argType == entity.EndpointArgTypeInteger {
			v, err := arg.Parse(arg.Default)
			if err != nil {
				defaultValue = 0
			} else {
				defaultValue = v.Value()
			}
		}
		schemaRef := &openapi3.SchemaRef{
			Value: &openapi3.Schema{
				Type:    argType,
				Example: defaultValue,
				Default: defaultValue,
			},
		}
		if len(arg.Enum) > 0 {
			for _, item := range strings.Split(arg.Enum, ",") {
				schemaRef.Value.Enum = append(schemaRef.Value.Enum, item)
			}
		}
		if arg.Type == entity.EndpointArgTypeArray {
			schemaRef.Value.Items = &openapi3.SchemaRef{
				Value: &openapi3.Schema{
					Type: arg.ItemType,
				},
			}
		}
		parameter := &openapi3.Parameter{
			Name:        arg.Name,
			In:          openapi3.ParameterInQuery,
			Description: arg.Description,
			Required:    required,
			Schema:      schemaRef,
		}
		if arg.IsPathParameter {
			parameter.In = openapi3.ParameterInPath
		}
		if arg.Type == entity.EndpointArgTypeArray {
			parameter.Style = "form"
			parameter.Explode = &explode
		}
		parameters = append(parameters, &openapi3.ParameterRef{
			Value: parameter,
		})
	}
	return parameters
}

func buildEndpointOperation(endpoint *entity.EndpointPublish, args []*entity.EndpointArgs) (*openapi3.Operation, error) {
	description, err := url.PathUnescape(endpoint.Description)
	if err != nil {
		return nil, errs.NewInternalUnknown(fmt.Sprintf("`%s` decode description failed", endpoint.Endpoint), err)
	}

	operationID := buildEndpointOperationID(endpoint)

	tag := endpoint.Tag
	if tag == "" {
		tag = "Default"
	}

	operation := openapi3.Operation{
		Summary:     endpoint.Name,
		Description: description,
		Tags:        []string{tag},
		OperationID: operationID,
		Responses: map[string]*openapi3.ResponseRef{
			"200": {
				Value: &openapi3.Response{
					Description: utils.Str2Ptr("OK"),
					Content: openapi3.Content{
						"application/json": {
							Schema: openapi3.NewSchemaRef(fmt.Sprintf("#/components/schemas/%sResponse", operationID), nil),
						},
					},
				},
			},
			"400": {
				Value: &openapi3.Response{
					Description: utils.Str2Ptr("Bad request"),
					Content: openapi3.Content{
						"application/json": {
							Schema:  openapi3.NewSchemaRef(fmt.Sprintf("#/components/schemas/%sResponse", operationID), nil),
							Example: buildErrorResponseExample(400, "param check failed! {detailed error}"),
						},
					},
				},
			},
			"401": {
				Value: &openapi3.Response{
					Description: utils.Str2Ptr("Unauthorized request"),
					Content: openapi3.Content{
						"application/json": {
							Schema:  openapi3.NewSchemaRef(fmt.Sprintf("#/components/schemas/%sResponse", operationID), nil),
							Example: buildErrorResponseExample(401, "auth failed"),
						},
					},
				},
			},
			"404": {
				Value: &openapi3.Response{
					Description: utils.Str2Ptr("The requested resource was not found"),
					Content: openapi3.Content{
						"application/json": {
							Schema:  openapi3.NewSchemaRef(fmt.Sprintf("#/components/schemas/%sResponse", operationID), nil),
							Example: buildErrorResponseExample(404, "endpoint not found"),
						},
					},
				},
			},
			"405": {
				Value: &openapi3.Response{
					Description: utils.Str2Ptr("The requested method is not supported for the specified resource"),
					Content: openapi3.Content{
						"application/json": {
							Schema:  openapi3.NewSchemaRef(fmt.Sprintf("#/components/schemas/%sResponse", operationID), nil),
							Example: buildErrorResponseExample(405, "method not allowed"),
						},
					},
				},
			},
			"408": {
				Value: &openapi3.Response{
					Description: utils.Str2Ptr("The server timed out waiting for the request"),
					Content: openapi3.Content{
						"application/json": {
							Schema:  openapi3.NewSchemaRef(fmt.Sprintf("#/components/schemas/%sResponse", operationID), nil),
							Example: buildErrorResponseExample(408, "request timeout"),
						},
					},
				},
			},
			"429": {
				Value: &openapi3.Response{
					Description: utils.Str2Ptr("The user has sent too many requests in a given amount of time"),
					Content: openapi3.Content{
						"application/json": {
							Schema:  openapi3.NewSchemaRef(fmt.Sprintf("#/components/schemas/%sResponse", operationID), nil),
							Example: buildErrorResponseExample(429, "The request exceeded the limit of 100 times per apikey per minute. For more quota, please contact us: https://support.pingcap.com/hc/en-us/requests/new?ticket_form_id=7800003722519"),
						},
					},
				},
			},
			"500": {
				Value: &openapi3.Response{
					Description: utils.Str2Ptr("Internal server error"),
					Content: openapi3.Content{
						"application/json": {
							Schema:  openapi3.NewSchemaRef(fmt.Sprintf("#/components/schemas/%sResponse", operationID), nil),
							Example: buildErrorResponseExample(500, "internal error! {detailed error}"),
						},
					},
				},
			},
		},
	}
	if endpoint.Method == http.MethodGet || endpoint.Method == http.MethodDelete {
		operation.Parameters = buildEndpointOperationParameters(endpoint, args)
	} else if len(args) > 0 {
		exmapleItem := make(map[string]string)
		parameters := make([]*openapi3.ParameterRef, 0, len(args))
		for _, arg := range args {
			if arg.IsPathParameter {
				argType := arg.Type
				var defaultValue any
				defaultValue = arg.Default
				if argType == entity.EndpointArgTypeInteger {
					v, err := arg.Parse(arg.Default)
					if err != nil {
						defaultValue = 0
					} else {
						defaultValue = v.Value()
					}
				}
				schemaRef := &openapi3.SchemaRef{
					Value: &openapi3.Schema{
						Type:    argType,
						Example: defaultValue,
						Default: defaultValue,
					},
				}
				parameter := &openapi3.Parameter{
					Name:        arg.Name,
					In:          openapi3.ParameterInPath,
					Description: arg.Description,
					Required:    true,
					Schema:      schemaRef,
				}
				parameters = append(parameters, &openapi3.ParameterRef{
					Value: parameter,
				})
			} else {
				exmapleItem[arg.Name] = arg.Default
			}
		}
		operation.Parameters = parameters
		if len(exmapleItem) > 0 {
			if !endpoint.IsBatch() {
				operation.RequestBody = &openapi3.RequestBodyRef{
					Value: &openapi3.RequestBody{
						Content: openapi3.Content{
							"application/json": {
								Schema:  openapi3.NewSchemaRef(fmt.Sprintf("#/components/schemas/%sRequestBody", operationID), nil),
								Example: exmapleItem,
							},
						},
					},
				}
			} else {
				example := map[string][]map[string]string{
					"items": {exmapleItem},
				}
				operation.RequestBody = &openapi3.RequestBodyRef{
					Value: &openapi3.RequestBody{
						Content: openapi3.Content{
							"application/json": {
								Schema:  openapi3.NewSchemaRef(fmt.Sprintf("#/components/schemas/%sRequestBody", operationID), nil),
								Example: example,
							},
						},
					},
				}
			}
		}
	}

	return &operation, nil
}

func buildErrorResponseExample(code int64, message string) *gatewaydto.AdvancedRes {
	return &gatewaydto.AdvancedRes{
		Type: "sql_endpoint",
		Data: entity.DataRes{
			Rows:    []entity.Row{},
			Columns: []entity.Column{},
			Result: entity.Result{
				Code:      code,
				Message:   message,
				StartMs:   0,
				EndMs:     0,
				Latency:   "",
				RowCount:  0,
				RowAffect: 0,
				Limit:     0,
			},
		},
	}
}

func getChat2QueryEDAAPISpec(ctx context.Context, dataapp *entity.DataApp) (*dto.OpenApiSpecification, error) {
	serverUrl := fmt.Sprintf("https://%s/api/v1beta/app/%s/endpoint", getEndpointHost(), dataapp.ExternalAppID)
	url := fmt.Sprintf("https://%s/eda/apidocs/swagger.json?server_url=%s", getEndpointHost(), serverUrl)

	client := http.DefaultClient
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return nil, err
	}

	req.Header.Set("X-Created-By", dataapp.CreatedBy)

	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	tilog.Info(ctx, "getChat2QueryEDAAPISpec", zap.String("url", url), zap.Int("status_code", resp.StatusCode))

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	var openapi3Struct openapi3.T
	err = json.Unmarshal(body, &openapi3Struct)
	if err != nil {
		return nil, err
	}

	spec := dto.OpenApiSpecification{
		Raw:      &openapi3Struct,
		Document: string(body),
		Filename: fmt.Sprintf("%s-openapi-specification.json", dataapp.ExternalAppID),
	}

	return &spec, nil
}

func GenerateOpenApiSpecification(ctx context.Context, appId uint64, cred *dataapi.Credential, format string) (*dto.OpenApiSpecification, error) {
	dataapp, err := repository.GetDataAppWithCtx(ctx, appId)
	if err != nil {
		return nil, errs.NewInternalDB("get dataapp failed", err)
	}

	if dataapp.AppType == entity.DataAppTypeChat2Query {
		if format != "json" {
			return nil, errs.NewBadRequest("only support json format", nil)
		}

		return getChat2QueryEDAAPISpec(ctx, dataapp)
	}

	dataappClusters, err := repository.ListAppClusterByAppId(ctx, appId)
	if err != nil {
		return nil, errs.NewInternalDB("list app clusters failed", err)
	}
	endpoints, err := repository.ListAppPublishedEndpoint(ctx, appId)
	fillPublishedEndpoints(ctx, endpoints)
	if err != nil {
		return nil, errs.NewInternalDB("list endpoints failed", err)
	}
	extractors := make(map[uint64]*oas.Extractor)
	for _, cluster := range dataappClusters {
		metaColumns, err := retriveTableMetaColumns(ctx, dataapp.ProjectId, cluster.ClusterID, cred)
		if err != nil {
			return nil, err
		}
		extractors[cluster.ClusterID] = oas.NewExtractor(metaColumns)
	}

	regionSuccessfulDomainsMap, err := GetRegionSuccessfulDomainsMap(ctx, appId)
	if err != nil {
		return nil, err
	}
	paths := make(map[string]*openapi3.PathItem)
	componentSchemas := make(openapi3.Schemas)
	scanedClusterIds := make(map[uint64]struct{})
	scanedServerUrls := make(map[string]struct{})
	servers := make(openapi3.Servers, 0, 4)
	for _, endpoint := range endpoints {
		// only generate doc for online endpoints
		// if endpoint.Label <= 0 {
		// 	tilog.Info(ctx, "skip sketch endpoint", zap.Uint64("endpoint_id", endpoint.ID))
		// 	continue
		// }

		pathItem, ok := paths[endpoint.Endpoint]
		if !ok {
			pathItem = new(openapi3.PathItem)
			paths[endpoint.Endpoint] = pathItem
		}

		var args []*entity.EndpointArgs
		err = json.Unmarshal([]byte(endpoint.Args), &args)
		if err != nil {
			return nil, errs.NewInternalUnknown(fmt.Sprintf("endpoint `%s:%s` parse parameters failed", endpoint.Method, endpoint.Endpoint), err)
		}

		operation, err := buildEndpointOperation(endpoint, args)
		if err != nil {
			return nil, err
		}
		switch endpoint.Method {
		case http.MethodGet:
			pathItem.Get = operation
		case http.MethodPost:
			pathItem.Post = operation
		case http.MethodPut:
			pathItem.Put = operation
		case http.MethodPatch:
			pathItem.Patch = operation
		case http.MethodDelete:
			pathItem.Delete = operation
		default:
			return nil, errs.NewInternalUnknown(fmt.Sprintf("endpoint `%s:%s` encounter unknown method", endpoint.Method, endpoint.Endpoint), err)
		}

		var columns oas.Columns
		if !entity.IsSystemDataEndpoint(endpoint.Type) {
			extractor, ok := extractors[endpoint.ClusterId]
			if !ok {
				tilog.Error(ctx, "get extractor failed", zap.Uint64("clusterId", endpoint.ClusterId))
				return nil, errs.NewBadRequest(fmt.Sprintf("endpoint `%s:%s` has no linked cluster", endpoint.Method, endpoint.Endpoint), nil)
			}
			columns, err = extractor.Extract(ctx, sqlArgsRegexp.ReplaceAllString(endpoint.SqlTemplate, "?"))
			if err != nil {
				return nil, errs.NewBadRequest(fmt.Sprintf("endpoint `%s:%s` extract columns from SQL failed: %s", endpoint.Method, endpoint.Endpoint, err), err)
			}
			if endpoint.IsBatch() && len(columns) == 0 {
				columns = oas.Columns{
					{ColumnName: "index"},
					{ColumnName: "success"},
					{ColumnName: "message"},
					{ColumnName: "auto_increment_id"},
				}
			}
		}
		operationID := buildEndpointOperationID(endpoint)
		componentSchemas[fmt.Sprintf("%sResponse", operationID)] = buildResponseSchema(columns)

		if endpoint.Method != http.MethodGet && endpoint.Method != http.MethodDelete && len(args) > 0 {
			componentSchemas[fmt.Sprintf("%sRequestBody", operationID)] = buildRequestBodySchema(endpoint.IsBatch(), args)
		}

		if !entity.IsSystemDataEndpoint(endpoint.Type) {
			if _, ok := scanedClusterIds[endpoint.ClusterId]; ok {
				tilog.Info(ctx, "cluster already scaned, skip it", zap.Uint64("clusterId", endpoint.ClusterId))
				continue
			}
			url, err := GetAPPServiceUrl(ctx, appId, endpoint.ClusterId, regionSuccessfulDomainsMap)
			if err != nil {
				tilog.Info(ctx, "get app service url failed, skip it", zap.Uint64("clusterId", endpoint.ClusterId))
				continue
			}
			if _, ok := scanedServerUrls[url]; ok {
				continue
			}
			servers = append(servers, &openapi3.Server{URL: url})
			scanedClusterIds[endpoint.ClusterId] = struct{}{}
			scanedServerUrls[url] = struct{}{}
		} else {
			for _, cluster := range dataappClusters {
				if _, ok := scanedClusterIds[cluster.ClusterID]; ok {
					tilog.Info(ctx, "cluster already scaned, skip it", zap.Uint64("clusterId", cluster.ClusterID))
					continue
				}
				url, err := GetAPPServiceUrl(ctx, appId, cluster.ClusterID, regionSuccessfulDomainsMap)
				if err != nil {
					tilog.Info(ctx, "get app service url failed, skip it", zap.Uint64("clusterId", cluster.ClusterID))
					continue
				}
				if _, ok := scanedServerUrls[url]; ok {
					continue
				}
				servers = append(servers, &openapi3.Server{URL: url})
				scanedClusterIds[endpoint.ClusterId] = struct{}{}
				scanedServerUrls[url] = struct{}{}
			}
		}
	}

	appVersion := dataapp.AppVersion
	if appVersion == "" {
		appVersion = "1.0.0"
	}
	root := openapi3.T{
		OpenAPI: "3.0.3",
		Info: &openapi3.Info{
			Title:       dataapp.AppName,
			Description: dataapp.Description,
			Version:     appVersion,
		},
		Servers: servers,
		Paths:   paths,
		Components: openapi3.Components{
			Schemas: componentSchemas,
			SecuritySchemes: openapi3.SecuritySchemes{
				"basicAuth": &openapi3.SecuritySchemeRef{
					Value: &openapi3.SecurityScheme{
						Type:        "http",
						Scheme:      "basic",
						Description: "Enter your public key for the username field and private key for the password field",
					},
				},
			},
		},
		Security: openapi3.SecurityRequirements{
			{
				"basicAuth": {},
			},
		},
	}

	var doc []byte
	if format == "yaml" {
		doc, err = yaml.Marshal(root)
		if err != nil {
			return nil, errs.NewInternalUnknown("marshal spec failed", err)
		}
	} else if format == "json" {
		doc, err = json.MarshalIndent(root, "", "  ")
		if err != nil {
			return nil, errs.NewInternalUnknown("marshal spec failed", err)
		}
	}
	spec := dto.OpenApiSpecification{
		Raw:      &root,
		Document: string(doc),
		Filename: fmt.Sprintf("%s-openapi-specification.%s", dataapp.ExternalAppID, format),
	}

	return &spec, nil
}

func ValidateAppOpenapiFormat(openapiFormat string) error {
	if openapiFormat != "json" && openapiFormat != "yaml" {
		return errs.NewBadRequest("OpenAPI Specification format must be one of `json` or `yaml`", nil)
	}
	return nil
}

func AuthorizeOpenApiSpecification(ctx context.Context, appId uint64) error {
	dataapp, err := repository.GetDataAppWithCtx(ctx, appId)
	if err != nil {
		return errs.NewInternalDB("get dataapp failed", err)
	}
	if dataapp.OasAuthorized {
		return nil
	}
	dataapp.OasAuthorized = true
	err = repository.UpdateDataApp(ctx, dataapp)
	if err != nil {
		return errs.NewInternalDB("update daataapp failed", err)
	}
	return nil
}
