package api

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strconv"

	scim2 "github.com/tidbcloud/account/internel/svc/scim"

	"github.com/gin-gonic/gin"
	"github.com/imulab/go-scim/pkg/v2/crud"
	"github.com/imulab/go-scim/pkg/v2/handlerutil"
	scimjson "github.com/imulab/go-scim/pkg/v2/json"
	"github.com/imulab/go-scim/pkg/v2/service"
	"github.com/imulab/go-scim/pkg/v2/spec"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

// okta protocol: https://developer.okta.com/docs/api/openapi/okta-scim/guides/scim-20/
// scim protocol： https://datatracker.ietf.org/doc/html/rfc7644#section-3.5.2
// azure protocol: https://learn.microsoft.com/en-us/entra/identity/app-provisioning/use-scim-to-provision-users-and-groups#disable-user

func RegisterScimRouter(e gin.IRouter) {
	scimGroupSvc, _ := scim2.GetScimGroupSvc()
	scimUserSvc, _ := scim2.GetScimUserSvc()

	r := e.Group("/orgs/:org_id/scim/v2")
	r.Use(scimReceiveHttpRequest)
	r.Use(scimProvisionCheckFunc)
	r.Use(scimAuthFunc)
	r.Use(scimRbacFunc)

	r.GET("/ServiceProviderConfig", ServiceProviderConfigHandler)
	r.GET("/Schemas", SchemasHandler)
	r.GET("/Schemas/:id", SchemaByIdHandler)
	r.GET("/ResourceTypes", ResourceTypesHandler)
	r.GET("/ResourceTypes/:id", ResourceTypeByIdHandler)

	r.GET("/Users", SearchHandler(scimUserSvc))
	r.GET("/Users/:id", GetHandler(scimUserSvc))
	r.POST("/Users", CreateHandler(scimUserSvc))
	r.PUT("/Users/:id", ReplaceHandler(scimUserSvc))
	r.PATCH("/Users/:id", PatchHandler(scimUserSvc))
	r.DELETE("/Users/:id", DeleteHandler(scimUserSvc))

	r.GET("/Groups", SearchHandler(scimGroupSvc))
	r.GET("/Groups/:id", GetHandler(scimGroupSvc))
	r.POST("/Groups", CreateHandler(scimGroupSvc))
	r.PUT("/Groups/:id", ReplaceHandler(scimGroupSvc))
	r.PATCH("/Groups/:id", PatchHandler(scimGroupSvc))
	r.DELETE("/Groups/:id", DeleteHandler(scimGroupSvc))

}

func init() {
	scim2.EnsureSchemaRegistered()
	initPermissionChecker()
}

func ServiceProviderConfigHandler(c *gin.Context) {
	config, err := scim2.ParseServiceProviderConfig()
	if err != nil {
		tilog.Error(c.Request.Context(), "ParseServiceProviderConfig error", zap.Any("err", err))
		_ = handlerutil.WriteError(c.Writer, err)
		return
	}
	raw, _ := json.Marshal(config)

	w := c.Writer
	w.Header().Set("Content-Type", spec.ApplicationScimJson)
	_, _ = w.Write(raw)
}

func SchemasHandler(c *gin.Context) {
	result, _ := scim2.ListSchemas()

	recorder := httptest.NewRecorder()
	_ = handlerutil.WriteSearchResultToResponse(recorder, result)

	rw := c.Writer
	rw.Header().Set("Content-Type", recorder.Header().Get("Content-Type"))
	_, _ = rw.Write(recorder.Body.Bytes())
}

func SchemaByIdHandler(c *gin.Context) {
	id := c.Param("id")

	schema, _ := scim2.GetSchemaByID(id)
	if schema == nil {
		tilog.Info(c.Request.Context(), "schema not found", zap.Any("id", id))
		_ = handlerutil.WriteError(c.Writer, fmt.Errorf("%w: schema is not found", spec.ErrNotFound))
		return
	}

	raw, err := scimjson.Serialize(schema)
	if err != nil {
		tilog.Info(c.Request.Context(), "serialize schema error", zap.Any("schema", schema), zap.Any("err", err))
		_ = handlerutil.WriteError(c.Writer, err)
		return
	}

	c.Writer.Header().Set("Content-Type", spec.ApplicationScimJson)
	_, _ = c.Writer.Write(raw)
}

func ResourceTypesHandler(c *gin.Context) {
	result, err := scim2.ListResourceTypes()
	if err != nil {
		_ = handlerutil.WriteError(c.Writer, fmt.Errorf("%w: list resource types error", spec.ErrInternal))
		tilog.Info(c.Request.Context(), "list resource types error", zap.Any("err", err))
		return
	}

	// use recorder to cache render result
	recorder := httptest.NewRecorder()
	if err := handlerutil.WriteSearchResultToResponse(recorder, result); err != nil {
		panic(err)
	}

	c.Writer.Header().Set("Content-Type", recorder.Header().Get("Content-Type"))
	_, _ = c.Writer.Write(recorder.Body.Bytes())

}

func ResourceTypeByIdHandler(c *gin.Context) {
	id := c.Param("id")
	resourceType, err := scim2.GetResourceTypeByID(id)
	if err != nil {
		tilog.Error(c.Request.Context(), "get resource type error", zap.Any("id", id), zap.Any("err", err))
		_ = handlerutil.WriteError(c.Writer, err)
		return
	}
	if resourceType == nil {
		tilog.Info(c.Request.Context(), "resource type is not found", zap.Any("id", id))
		_ = handlerutil.WriteError(c.Writer, fmt.Errorf("%w: resource type is not found", spec.ErrNotFound))
		return
	}

	raw, _ := scimjson.Serialize(resourceType)

	c.Writer.Header().Set("Content-Type", spec.ApplicationScimJson)
	_, _ = c.Writer.Write(raw)

}

func writeSearchResultToResponse(rw http.ResponseWriter, searchResult *scim2.ScimQueryResponse) error {
	render := handlerutil.SearchResultRendering{
		Schemas:      []string{"urn:ietf:params:scim:api:messages:2.0:ListResponse"},
		TotalResults: searchResult.TotalResults,
		StartIndex:   searchResult.StartIndex,
		ItemsPerPage: searchResult.ItemsPerPage,
		Resources:    []json.RawMessage{},
	}

	for _, resource := range searchResult.Resources {
		raw, _ := json.Marshal(&resource)
		render.Resources = append(render.Resources, raw)
	}

	rw.Header().Set("Content-Type", spec.ApplicationScimJson)
	return json.NewEncoder(rw).Encode(render)
}

func getOrgId(c *gin.Context) (uint64, error) {
	orgIdStr := c.Param("org_id")
	orgID, err := strconv.ParseUint(orgIdStr, 10, 64)
	return orgID, err
}

func SearchHandler(svc scim2.SearchScimResources) func(c *gin.Context) {
	return func(c *gin.Context) {
		req, err := handlerutil.QueryRequestFromGet(c.Request)
		if err != nil {
			tilog.Info(c.Request.Context(), "SearchHandler query request from get error", zap.Any("err", err))
			_ = handlerutil.WriteError(c.Writer, err)
			return
		}
		tilog.Info(c.Request.Context(), "SearchHandler list scim resources request", zap.Any("req", req))
		if req.Pagination == nil {
			req.Pagination = &crud.Pagination{
				StartIndex: 1,
				Count:      100,
			}
		} else {
			if req.Pagination.StartIndex <= 0 {
				req.Pagination.StartIndex = 1
			}
			if req.Pagination.Count <= 0 {
				req.Pagination.Count = 100
			}
		}
		orgId, _ := getOrgId(c)

		rsp, err := svc.SearchResources(c.Request.Context(), orgId, req)
		if err != nil {
			_ = handlerutil.WriteError(c.Writer, err)
			return
		}
		_ = writeSearchResultToResponse(c.Writer, rsp)
	}
}

func CreateHandler(svc scim2.CreateScimResource) func(c *gin.Context) {
	return func(c *gin.Context) {
		cr, closer := handlerutil.CreateRequest(c.Request)
		defer closer()
		orgID, _ := getOrgId(c)

		rsp, err := svc.CreateResource(c.Request.Context(), orgID, cr)
		if err != nil {
			_ = handlerutil.WriteError(c.Writer, err)
			return
		}
		c.Writer.WriteHeader(201)
		_ = writeResourceToResponse(c.Writer, rsp)
	}
}

func GetHandler(svc scim2.GetScimResource) func(c *gin.Context) {
	return func(c *gin.Context) {
		resourceID := c.Param("id")
		tilog.Info(c.Request.Context(), "GetHandler get scim resource request", zap.Any("id", resourceID))
		orgID, _ := getOrgId(c)

		rsp, err := svc.GetResource(c.Request.Context(), orgID, &service.GetRequest{
			ResourceID: resourceID,
		})
		if err != nil {
			_ = handlerutil.WriteError(c.Writer, err)
			return
		}
		_ = writeResourceToResponse(c.Writer, rsp)
	}

}

func writeResourceToResponse(rw http.ResponseWriter, resource map[string]interface{}) error {
	raw, jsonErr := json.Marshal(&resource)
	if jsonErr != nil {
		return jsonErr
	}

	rw.Header().Set("Content-Type", spec.ApplicationScimJson)
	_, writeErr := rw.Write(raw)
	return writeErr
}

func ReplaceHandler(svc scim2.ReplaceScimResource) func(c *gin.Context) {
	return func(c *gin.Context) {
		resourceID := c.Param("id")
		reqFunc, closer := handlerutil.ReplaceRequest(c.Request)
		defer closer()
		req := reqFunc(resourceID)
		orgID, _ := getOrgId(c)

		rsp, err := svc.ReplaceResource(c.Request.Context(), orgID, req)
		if err != nil {
			_ = handlerutil.WriteError(c.Writer, err)
			return
		}
		_ = writeResourceToResponse(c.Writer, rsp.Resource)
	}
}

func PatchHandler(svc scim2.PatchScimResource) func(c *gin.Context) {
	return func(c *gin.Context) {
		resourceID := c.Param("id")
		reqFunc, closer := handlerutil.PatchRequest(c.Request)
		defer closer()
		req := reqFunc(resourceID)
		orgID, _ := getOrgId(c)

		rsp, err := svc.PatchResource(c.Request.Context(), orgID, req)
		if err != nil {
			_ = handlerutil.WriteError(c.Writer, err)
			return
		}
		if !rsp.Updated {
			c.Writer.WriteHeader(204)
			return
		}
		_ = writeResourceToResponse(c.Writer, rsp.Resource)
	}
}

func DeleteHandler(svc scim2.DeleteScimResource) func(c *gin.Context) {
	return func(c *gin.Context) {
		resourceID := c.Param("id")
		reqFunc := handlerutil.DeleteRequest(c.Request)
		req := reqFunc(resourceID)
		orgID, _ := getOrgId(c)

		_, err := svc.DeleteResource(c.Request.Context(), orgID, req)
		if err != nil {
			_ = handlerutil.WriteError(c.Writer, err)
			return
		}
		c.Writer.WriteHeader(204)
	}
}
