package internal_api

import (
	"errors"
	"net/http"
	"sort"
	"strconv"
	"strings"
	"time"
	"unicode"

	"github.com/gin-gonic/gin"
	"github.com/pingcap/log"
	"go.uber.org/zap"
	"gorm.io/gorm"

	error2 "github.com/pingcap-inc/clinic/pkg/api/error"
	"github.com/pingcap-inc/clinic/pkg/api/healthcheck"
	"github.com/pingcap-inc/clinic/pkg/db"
	"github.com/pingcap-inc/clinic/pkg/db/model"
)

const (
	defaultHealthCheckReportPageSize = 20
	maxHealthCheckReportPageSize     = 100
	maxHealthCheckReportItemNames    = 64
)

var healthCheckReportStatuses = map[string]struct{}{
	string(db.StatusPending):       {},
	string(db.StatusRunning):       {},
	string(db.StatusPartial):       {},
	string(db.StatusSuccess):       {},
	string(db.StatusPartialFailed): {},
	string(db.StatusFailed):        {},
}

type healthCheckReportQuery interface {
	ListReports(string, string, healthcheck.HealthCheckReportListOptions) (*healthcheck.HealthCheckReportPage, error)
	GetReport(string, string, string, []string) (*healthcheck.HealthCheckReport, error)
}

type healthCheckReportOrganizationResolver func(string) (uint, error)

type DataProxyHealthCheckReportService struct {
	query                       healthCheckReportQuery
	resolveClinicOrganizationID healthCheckReportOrganizationResolver
}

func NewDataProxyHealthCheckReportService(query *healthcheck.HealthCheckReportQueryService) *DataProxyHealthCheckReportService {
	return newDataProxyHealthCheckReportService(query, model.GetOrgIDByDbaasOrgID)
}

func newDataProxyHealthCheckReportService(
	query healthCheckReportQuery,
	resolver healthCheckReportOrganizationResolver,
) *DataProxyHealthCheckReportService {
	return &DataProxyHealthCheckReportService{
		query:                       query,
		resolveClinicOrganizationID: resolver,
	}
}

func (s *DataProxyHealthCheckReportService) ListReports(c *gin.Context) {
	orgID, ok := parseDataProxyOrgID(c)
	if !ok {
		return
	}
	clusterID := c.Param("clusterId")
	if err := validateClusterMetadataID(clusterID); err != nil {
		_ = c.Error(error2.ErrBadRequest.New("%s", err.Error()))
		return
	}
	if _, ok := dataProxyInternalUser(c); !ok {
		return
	}
	options, err := parseHealthCheckReportListOptions(c)
	if err != nil {
		_ = c.Error(error2.ErrBadRequest.New("%s", err.Error()))
		return
	}

	if s.query == nil {
		_ = c.Error(error2.ErrServiceUnavailable.New("health check report query service is unavailable"))
		return
	}
	canonicalOrgID := formatDataProxyOrgID(orgID)
	clinicOrgID, ok := s.resolveReportOrganizationID(c, canonicalOrgID)
	if !ok {
		return
	}
	page, err := s.query.ListReports(
		clinicOrgID,
		clusterID,
		options,
	)
	if err != nil {
		s.writeQueryError(c, clusterID, "", err)
		return
	}
	if page == nil {
		s.writeQueryError(c, clusterID, "", errors.New("health check report query returned an empty page"))
		return
	}
	for i := range page.Items {
		page.Items[i].OrgID = canonicalOrgID
		page.Items[i].PortalOrgID = clinicOrgID
	}
	c.JSON(http.StatusOK, page)
}

func (s *DataProxyHealthCheckReportService) GetReport(c *gin.Context) {
	orgID, ok := parseDataProxyOrgID(c)
	if !ok {
		return
	}
	clusterID := c.Param("clusterId")
	if err := validateClusterMetadataID(clusterID); err != nil {
		_ = c.Error(error2.ErrBadRequest.New("%s", err.Error()))
		return
	}
	reportID := c.Param("reportId")
	if err := validateHealthCheckReportID(reportID); err != nil {
		_ = c.Error(error2.ErrBadRequest.New("%s", err.Error()))
		return
	}
	if _, ok := dataProxyInternalUser(c); !ok {
		return
	}
	itemNames, err := parseHealthCheckReportItemNames(c)
	if err != nil {
		_ = c.Error(error2.ErrBadRequest.New("%s", err.Error()))
		return
	}

	if s.query == nil {
		_ = c.Error(error2.ErrServiceUnavailable.New("health check report query service is unavailable"))
		return
	}
	canonicalOrgID := formatDataProxyOrgID(orgID)
	clinicOrgID, ok := s.resolveReportOrganizationID(c, canonicalOrgID)
	if !ok {
		return
	}
	report, err := s.query.GetReport(clinicOrgID, clusterID, reportID, itemNames)
	if err != nil {
		s.writeQueryError(c, clusterID, reportID, err)
		return
	}
	if report == nil {
		s.writeQueryError(c, clusterID, reportID, errors.New("health check report query returned an empty report"))
		return
	}
	report.OrgID = canonicalOrgID
	report.PortalOrgID = clinicOrgID
	c.JSON(http.StatusOK, report)
}

func (s *DataProxyHealthCheckReportService) resolveReportOrganizationID(
	c *gin.Context,
	canonicalOrgID string,
) (string, bool) {
	// Portal-created Health Check rows use Clinic's organization primary key,
	// while canonical Data Proxy paths use the DBAAS organization ID.
	if s.resolveClinicOrganizationID == nil {
		_ = c.Error(error2.ErrServiceUnavailable.New("health check report organization resolver is unavailable"))
		return "", false
	}
	clinicOrgID, err := s.resolveClinicOrganizationID(canonicalOrgID)
	if errors.Is(err, gorm.ErrRecordNotFound) {
		_ = c.Error(error2.ErrNotFound.New("organization not found"))
		return "", false
	}
	if err != nil {
		log.Error("resolve health check report organization failed",
			zap.String("dbaas_org_id", canonicalOrgID), zap.Error(err))
		_ = c.Error(error2.ErrInternal.New("failed to resolve organization"))
		return "", false
	}
	return strconv.FormatUint(uint64(clinicOrgID), 10), true
}

func (s *DataProxyHealthCheckReportService) writeQueryError(
	c *gin.Context,
	clusterID, reportID string,
	err error,
) {
	switch {
	case errors.Is(err, healthcheck.ErrHealthCheckReportNotFound):
		_ = c.Error(error2.ErrNotFound.New("health check report not found"))
	case errors.Is(err, healthcheck.ErrHealthCheckReportItemNotFound):
		_ = c.Error(error2.ErrBadRequest.New("requested health check report item not found"))
	case errors.Is(err, healthcheck.ErrHealthCheckReportStorageUnavailable):
		log.Warn("health check report storage is unavailable",
			zap.String("cluster_id", clusterID), zap.String("report_id", reportID), zap.Error(err))
		_ = c.Error(error2.ErrServiceUnavailable.New("health check report storage is temporarily unavailable"))
	default:
		log.Error("query health check reports failed",
			zap.String("cluster_id", clusterID), zap.String("report_id", reportID), zap.Error(err))
		_ = c.Error(error2.ErrInternal.New("failed to query health check reports"))
	}
}

func parseHealthCheckReportListOptions(c *gin.Context) (healthcheck.HealthCheckReportListOptions, error) {
	values := c.Request.URL.Query()
	if err := rejectUnexpectedHealthCheckReportQuery(values, map[string]struct{}{
		"page": {}, "pageSize": {}, "status": {}, "createdFrom": {}, "createdTo": {},
	}); err != nil {
		return healthcheck.HealthCheckReportListOptions{}, err
	}
	pageRaw, err := singleHealthCheckReportQuery(values, "page")
	if err != nil {
		return healthcheck.HealthCheckReportListOptions{}, err
	}
	page, err := parseClusterMetadataPositiveInt(pageRaw, 1, maxClusterMetadataPage)
	if err != nil {
		return healthcheck.HealthCheckReportListOptions{}, errors.New("page must be between 1 and 1000000")
	}
	pageSizeRaw, err := singleHealthCheckReportQuery(values, "pageSize")
	if err != nil {
		return healthcheck.HealthCheckReportListOptions{}, err
	}
	pageSize, err := parseClusterMetadataPositiveInt(pageSizeRaw, defaultHealthCheckReportPageSize, maxHealthCheckReportPageSize)
	if err != nil {
		return healthcheck.HealthCheckReportListOptions{}, errors.New("pageSize must be between 1 and 100")
	}
	statuses, err := parseHealthCheckReportStatuses(values["status"])
	if err != nil {
		return healthcheck.HealthCheckReportListOptions{}, err
	}
	createdFromRaw, err := singleHealthCheckReportQuery(values, "createdFrom")
	if err != nil {
		return healthcheck.HealthCheckReportListOptions{}, err
	}
	createdFrom, err := parseOptionalHealthCheckReportTime(createdFromRaw, "createdFrom")
	if err != nil {
		return healthcheck.HealthCheckReportListOptions{}, err
	}
	createdToRaw, err := singleHealthCheckReportQuery(values, "createdTo")
	if err != nil {
		return healthcheck.HealthCheckReportListOptions{}, err
	}
	createdTo, err := parseOptionalHealthCheckReportTime(createdToRaw, "createdTo")
	if err != nil {
		return healthcheck.HealthCheckReportListOptions{}, err
	}
	if createdFrom != nil && createdTo != nil && createdFrom.After(*createdTo) {
		return healthcheck.HealthCheckReportListOptions{}, errors.New("createdFrom must not be after createdTo")
	}
	return healthcheck.HealthCheckReportListOptions{
		Page: page, PageSize: pageSize, Statuses: statuses, CreatedFrom: createdFrom, CreatedTo: createdTo,
	}, nil
}

func parseHealthCheckReportStatuses(values []string) ([]string, error) {
	unique := make(map[string]struct{})
	for _, value := range values {
		for _, candidate := range strings.Split(value, ",") {
			status := strings.TrimSpace(candidate)
			if status == "" {
				return nil, errors.New("status must not be empty")
			}
			if _, ok := healthCheckReportStatuses[status]; !ok {
				return nil, errors.New("status contains an unsupported health check report status")
			}
			unique[status] = struct{}{}
		}
	}
	statuses := make([]string, 0, len(unique))
	for status := range unique {
		statuses = append(statuses, status)
	}
	sort.Strings(statuses)
	return statuses, nil
}

func parseOptionalHealthCheckReportTime(raw, name string) (*time.Time, error) {
	if raw == "" {
		return nil, nil
	}
	value, err := time.Parse(time.RFC3339, raw)
	if err != nil {
		return nil, errors.New(name + " must be an RFC3339 timestamp")
	}
	value = value.UTC()
	return &value, nil
}

func parseHealthCheckReportItemNames(c *gin.Context) ([]string, error) {
	valuesByName := c.Request.URL.Query()
	if err := rejectUnexpectedHealthCheckReportQuery(valuesByName, map[string]struct{}{"itemName": {}}); err != nil {
		return nil, err
	}
	values := valuesByName["itemName"]
	if len(values) > maxHealthCheckReportItemNames {
		return nil, errors.New("itemName may be repeated at most 64 times")
	}
	unique := make(map[string]struct{}, len(values))
	items := make([]string, 0, len(values))
	for _, raw := range values {
		name := strings.TrimSpace(raw)
		if err := validateHealthCheckReportID(name); err != nil {
			return nil, errors.New("itemName must be a non-empty identifier no longer than 255 bytes")
		}
		if _, ok := unique[name]; ok {
			continue
		}
		unique[name] = struct{}{}
		items = append(items, name)
	}
	return items, nil
}

func rejectUnexpectedHealthCheckReportQuery(values map[string][]string, allowed map[string]struct{}) error {
	for key := range values {
		if _, ok := allowed[key]; !ok {
			return errors.New("unsupported query parameter: " + key)
		}
	}
	return nil
}

func singleHealthCheckReportQuery(values map[string][]string, name string) (string, error) {
	raw := values[name]
	if len(raw) == 0 {
		return "", nil
	}
	if len(raw) != 1 {
		return "", errors.New(name + " must be specified once")
	}
	return raw[0], nil
}

func validateHealthCheckReportID(value string) error {
	if value == "" || strings.TrimSpace(value) != value {
		return errors.New("reportId is required")
	}
	if len(value) > maxClusterMetadataTextLength || strings.Contains(value, "/") {
		return errors.New("reportId must be a non-empty identifier no longer than 255 bytes")
	}
	for _, r := range value {
		if unicode.IsControl(r) {
			return errors.New("reportId must not contain control characters")
		}
	}
	return nil
}

func formatDataProxyOrgID(value uint64) string {
	return strconv.FormatUint(value, 10)
}
