package auth

import (
	"bytes"
	stderrors "errors"
	"fmt"
	"io"
	"net/http"
	"strconv"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/grafana/regexp"
	"github.com/pingcap/errors"
	"github.com/pingcap/log"
	"github.com/tidbcloud/dataflow-service-ng/app/container"
	changefeedopenpb "github.com/tidbcloud/dataflow-service-ng/idl/pbgen/proto/openapi/v1beta2"
	mgmt "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
	accountAuth "github.com/tidbcloud/tiinf/sdk/auth"
	"go.uber.org/zap"
	"google.golang.org/protobuf/encoding/protojson"
)

const (
	CTX_KEY_RESOURCE_ORG_ID  = "ctx_auth_org_id"
	CTX_KEY_RESOURCE_PRJ_ID  = "ctx_auth_project_id"
	CTX_KEY_RESOURCE_TIDB_ID = "ctx_auth_tidb_id"
)

type errWithHttpCode struct {
	err      error
	httpCode int
}

func (e *errWithHttpCode) Error() string {
	return e.err.Error()
}

func (e *errWithHttpCode) Unwrap() error {
	return e.err
}

func newErrWithHttpCode(err error, httpCode int) error {
	return &errWithHttpCode{err: err, httpCode: httpCode}
}

func getHttpCode(err error) int {
	var target *errWithHttpCode
	if stderrors.As(err, &target) && target.httpCode != 0 {
		return target.httpCode
	}
	return http.StatusInternalServerError
}

func SetAuthIDsWithTiDBId(c *gin.Context) error {
	re := regexp.MustCompile(`tidbs/(\d+)`)
	matches := re.FindStringSubmatch(c.Request.URL.Path)

	if len(matches) > 1 {
		tidbID, err := strconv.ParseUint(matches[1], 10, 64)
		if err != nil {
			return err
		}
		return setAuthIDs(c, tidbID)
	}
	return nil
}

// SetAuthIDsWithChangefeed resolves the upstream TiDB cluster for
// top-level Premium changefeed routes, then populates the same auth context as
// SetAuthIDsWithTiDBId.
func SetAuthIDsWithChangefeed(c *gin.Context) error {
	path := c.Request.URL.Path
	if !strings.HasPrefix(path, "/v1beta2/changefeeds") {
		return nil
	}

	var tidbID uint64
	if path == "/v1beta2/changefeeds" {
		switch c.Request.Method {
		case http.MethodGet:
			rawTiDBID := c.Query("tidbId")
			if rawTiDBID == "" {
				rawTiDBID = c.Query("tidb_id")
			}
			id, err := strconv.ParseUint(rawTiDBID, 10, 64)
			if err != nil {
				return newErrWithHttpCode(fmt.Errorf("invalid tidbId: %w", err), http.StatusBadRequest)
			}
			tidbID = id
		case http.MethodPost:
			body, err := io.ReadAll(c.Request.Body)
			if err != nil {
				return err
			}
			c.Request.Body = io.NopCloser(bytes.NewReader(body))
			req := new(changefeedopenpb.CreateChangefeedRequest)
			unmarshalOpts := protojson.UnmarshalOptions{
				DiscardUnknown: true,
			}
			if err := unmarshalOpts.Unmarshal(body, req,); err != nil {
				return newErrWithHttpCode(fmt.Errorf("decode create changefeed request: %w", err), http.StatusBadRequest)
			}
			id, err := strconv.ParseUint(req.GetChangefeed().GetTidbId(), 10, 64)
			if err != nil {
				return newErrWithHttpCode(fmt.Errorf("invalid changefeed.tidb_id: %w", err), http.StatusBadRequest)
			}
			tidbID = id
		}
	} else {
		matches := regexp.MustCompile(`/v1beta2/changefeeds/(\d+)`).FindStringSubmatch(path)
		if len(matches) <= 1 {
			return nil
		}
		changefeedID, err := strconv.ParseUint(matches[1], 10, 64)
		if err != nil {
			return newErrWithHttpCode(err, http.StatusBadRequest)
		}
		changefeed, err := container.Global().BizChangefeedRepo.Load(c.Request.Context(), changefeedID)
		if err != nil {
			return err
		}
		tidbID = changefeed.ClusterID.Uint64()
	}

	if tidbID == 0 {
		return newErrWithHttpCode(fmt.Errorf("no TiDB cluster resolved for changefeed route"), http.StatusBadRequest)
	}
	return setAuthIDs(c, tidbID)
}

func setAuthIDs(c *gin.Context, tidbID uint64) error {
	resp, err := container.Global().MgmtSvcGrpcClient.ListClusters(
		c.Request.Context(),
		&mgmt.ListClustersRequest{Filter: &mgmt.ClusterFilter{ClusterIds: []uint64{tidbID}}},
	)
	if err != nil {
		return err
	}
	if len(resp.Clusters) == 0 {
		return newErrWithHttpCode(fmt.Errorf("no cluster"), http.StatusForbidden)
	}
	orgID := resp.Clusters[0].GetOrganizationId()
	projectID := resp.Clusters[0].GetProjectId()
	query := c.Request.URL.Query()
	query.Set(accountAuth.AccessOrgIdQueryKey, strconv.FormatUint(orgID, 10))
	c.Request.URL.RawQuery = query.Encode()
	c.Set(CTX_KEY_RESOURCE_ORG_ID, orgID)
	c.Set(CTX_KEY_RESOURCE_PRJ_ID, projectID)
	c.Set(CTX_KEY_RESOURCE_TIDB_ID, tidbID)
	log.L().Info("set auth orgId with TiDBId",
		zap.Uint64("orgId", orgID), zap.Uint64("prjId", projectID), zap.Uint64("TiDBId", tidbID))
	return nil
}

func GenAccessParam4GinWithPrefixIDs(c *gin.Context) (*accountAuth.AccessParam, error) {
	authInfo, err := accountAuth.GetIdentityInfoFromGinContext(c)
	if err != nil {
		return nil, err
	}
	var orgId uint64
	switch authInfo.IdentityType {
	case accountAuth.AuthnIdentityTypeApiKeyAK:
		orgId, _ = accountAuth.GetOrgIdFromApiKeyRsp(c.Request.Context(), authInfo.ApiKeyRsp)
		if orgId == 0 {
			orgId, _ = accountAuth.GetAccessOrgIdFromGinContext(c)
		}
	case accountAuth.AuthnIdentityTypeAccessToken:
		orgId = authInfo.AccessTokenRsp.AccessTokenIdentity.OrgId
	case accountAuth.AuthnIdentityTypeUserToken:
		orgId, _ = accountAuth.GetAccessOrgIdFromGinContext(c)
	default:
		return nil, errors.New("invalid authn identity type")
	}
	prjId := c.GetUint64(CTX_KEY_RESOURCE_PRJ_ID)
	tidbID := c.GetUint64(CTX_KEY_RESOURCE_TIDB_ID)
	if orgId == 0 || prjId == 0 || tidbID == 0 {
		return nil, errors.New("missing org_id or project_id or tidb_id for access check")
	}

	var accessMetadataPath string
	if strings.HasPrefix(c.Request.URL.Path, "/v1beta2/changefeeds") {
		accessMetadataPath = fmt.Sprintf(
			"/orgs/%d/projects/%d/tidbs/%d%s",
			orgId, prjId, tidbID, c.Request.URL.Path,
		)
	} else {
		accessMetadataPath = fmt.Sprintf(
			"/orgs/%d/projects/%d%s",
			orgId, prjId, c.Request.URL.Path,
		)
	}

	param := &accountAuth.AccessParam{
		Metadata: map[string]interface{}{
			accountAuth.AccessMetadataPath:   accessMetadataPath,
			accountAuth.AccessMetadataMethod: c.Request.Method,
		},
		Req: c,
	}
	return param, nil
}
