package controller

import (
	"bytes"
	"context"
	"encoding/csv"
	"encoding/json"
	"fmt"
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"github.com/tidbcloud/tiinf/sdk/auditlog"
	"github.com/tidbcloud/tiinf/transport/http/tigin"
	"go.uber.org/zap"
	"gorm.io/gorm"

	aclpb "github.com/tidbcloud/account/idl/pbgen/proto/acl"

	"github.com/tidbcloud/auditlog/configs"
	"github.com/tidbcloud/auditlog/consts"
	"github.com/tidbcloud/auditlog/internal/dao"
	"github.com/tidbcloud/auditlog/internal/dto"
	"github.com/tidbcloud/auditlog/internal/service"
	"github.com/tidbcloud/auditlog/third_party/account"
	"github.com/tidbcloud/auditlog/third_party/acl"
	"github.com/tidbcloud/auditlog/third_party/cluster_management"
	"github.com/tidbcloud/auditlog/types"
)

var (
	auditSvc service.AuditSvc
)

func InitAuditService(ctx context.Context, cfg *configs.Config, db *gorm.DB, auditDao dao.AuditDaoIface) {
	accountSvc := account.NewAccountSvc(ctx, cfg)
	cmSvc := cluster_management.NewCMSvc(ctx, cfg)
	aclSvc := acl.NewAclSvcIface(ctx, cfg)

	auditSvc = service.NewAuditSvc(db, auditDao, accountSvc, cmSvc, aclSvc)
}

// @Summary query event types
// @Tags auditlog
// @Router /event_types [get]
// @Produce json
// @Success 200 {object} types.EventTypesResp
func HandleQueryEventTypes(c *gin.Context) {
	c.JSON(http.StatusOK, types.EventTypesResp{EventTypes: consts.EventCodeMapper})
}

func checkIfHavePermission(c *gin.Context) (tigin.URIParams, bool) {
	uriParams := tigin.URIParams{}
	if err := c.ShouldBindUri(&uriParams); err != nil {
		tigin.AbortWithError(c, errs.NewBadRequest("invalid url", nil))
		return uriParams, false
	}
	if uriParams.OrgID == 0 {
		tigin.AbortWithError(c, errs.NewBadRequest("org_id is invalid", nil))
		return uriParams, false
	}

	// check if it's owner
	user, err := tigin.GetCurrentUser(c)
	if err != nil {
		tigin.AbortWithError(c, err)
		return uriParams, false
	}
	ctx := c.Request.Context()
	cfg := configs.GetConfig()
	accountSvc := account.NewAccountSvc(ctx, cfg)

	orgRole, err := accountSvc.GetUserRoleInOrg(ctx, user.Email, uriParams.OrgID)
	if err != nil {
		tigin.AbortWithError(c, err)
		return uriParams, false
	}

	if !(orgRole == aclpb.OrgRole_ORG_ROLE_OWNER || orgRole == aclpb.OrgRole_ORG_ROLE_AUDIT_ADMIN) {
		tigin.AbortWithError(c, errs.NewForbidden("permission denied", nil))
		return uriParams, false
	}

	return uriParams, true
}

// @Summary query settings
// @Tags auditlog
// @Router /:org_id/settings [get]
// @Produce json
// @Success 200 {object} types.QuerySettingsResp
func HandleQuerySettings(c *gin.Context) {
	ctx := c.Request.Context()
	uriParams, ok := checkIfHavePermission(c)
	if !ok {
		tilog.Error(ctx, "current user is not owner, reject to service")
		return
	}

	settings, err := auditSvc.QueryOrgSettings(ctx, uriParams.OrgID)
	if err != nil && err != gorm.ErrRecordNotFound {
		tigin.AbortWithError(c, errs.NewInternalDB("failed to get settings", err))
		return
	}

	status := consts.SettingsOff
	if settings != nil {
		status = settings.Status
	}

	resp := types.QuerySettingsResp{Status: status}

	c.JSON(http.StatusOK, resp)
}

func HandleOpsQuerySettings(c *gin.Context) {
	ctx := c.Request.Context()
	uriParams := tigin.URIParams{}
	if err := c.ShouldBindUri(&uriParams); err != nil {
		tigin.AbortWithError(c, errs.NewBadRequest("invalid url", nil))
		return
	}
	if uriParams.OrgID == 0 {
		tigin.AbortWithError(c, errs.NewBadRequest("org_id is invalid", nil))
		return
	}

	settings, err := auditSvc.QueryOrgSettings(ctx, uriParams.OrgID)
	if err != nil && err != gorm.ErrRecordNotFound {
		tigin.AbortWithError(c, errs.NewInternalDB("failed to get settings", err))
		return
	}

	status := consts.SettingsOff
	if settings != nil {
		status = settings.Status
	}

	resp := types.QuerySettingsResp{Status: status}

	c.JSON(http.StatusOK, resp)
}

// @Summary update settings
// @Tags auditlog
// @Router /:org_id/settings [put]
// @Param status body types.UpdateSettingsReq true "update settings"
// @Produce json
// @Success 200 {object} nil
func HandleUpdateSettings(c *gin.Context) {
	ctx := c.Request.Context()
	uriParams, ok := checkIfHavePermission(c)
	if !ok {
		tilog.Error(ctx, "current user is not owner, reject to service")
		return
	}

	payload := types.UpdateSettingsReq{}
	if err := c.ShouldBindJSON(&payload); err != nil {
		tigin.AbortWithError(c, errs.NewBadRequest("invalid requests", err))
		return
	}

	if !(payload.Status == consts.SettingsOff || payload.Status == consts.SettingsOn) {
		tigin.AbortWithError(c, errs.NewBadRequest("invalid requests", nil))
		return
	}

	err := auditSvc.UpdateOrgSettings(ctx, uriParams.OrgID, payload.Status)
	action := "enable"
	if payload.Status == consts.SettingsOff {
		action = "disable"
	}
	details := auditlog.Details{
		auditlog.KeyEnableOrDisable: action,
		auditlog.KeyError:           err,
	}
	auditlog.NewEvent(ctx, auditlog.EventEnableConsoleAuditLog).ParseGIN(c).SetDetails(details.Build()).SetResult(err == nil).SetOrgID(uriParams.OrgID).Submit() //nolint:errcheck

	c.JSON(http.StatusOK, nil)
}

// @Summary query audit log
// @Tags auditlog
// @Accept json
// @Produce json
// @Param org_id path uint64 true "org id"
// @Param event_types query string false "event types"
// @Param result query int false "int"
// @Param started_at query int64 false "time range, started_at"
// @Param ended_at query int64 false "time range, ended_at"
// @Param operator_type query uint8 false "operator type"
// @Param operator_id query uint64 false "operator id"
// @Param operator_email query string false "operator email"
// @Param operator_ip query string false "operator ip"
// @Param operator_login_method query string false "login method: google/github/auth0"
// @Param trace_id query string false "trace id"
// @Param org_id query uint64 false "org_id"
// @Param org_name query string false "org name"
// @Param project_id query uint64 false "project id"
// @Param project_name query string false "project name"
// @Param cluster_id query uint64 false "cluster id"
// @Param cluster_name query string false "cluster name"
// @Param page query uint64 false "page number"
// @Param per_page query uint64 false "page size"
// @Router /:org_id/audit_logs [get]
// @Produce json
// @Success 200 {object} types.QueryAuditLogResp
func HandleQueryAuditLog(c *gin.Context) {
	ctx := c.Request.Context()
	uriParams, ok := checkIfHavePermission(c)
	if !ok {
		tilog.Error(ctx, "current user is not owner, reject to service")
		return
	}

	var payload types.QueryAuditLogFilter
	if err := c.ShouldBindQuery(&payload); err != nil {
		tigin.AbortWithError(c, errs.NewBadRequest(err.Error(), err))
		return
	}

	// overwrite filters
	payload.OrgID = uriParams.OrgID

	events, count, err := auditSvc.QueryEvents(ctx, payload)
	if err != nil {
		tigin.AbortWithError(c, err)
		return
	}

	var exportEvents []types.ExportEvent
	for _, e := range events {
		exportEvents = append(exportEvents, dto.FormatEvent(e))
	}

	resp := types.QueryAuditLogResp{
		Events:     exportEvents,
		TotalCount: count,
	}
	c.JSON(http.StatusOK, resp)
}

func HandleOpsQueryAuditLog(c *gin.Context) {
	ctx := c.Request.Context()
	uriParams := tigin.URIParams{}
	if err := c.ShouldBindUri(&uriParams); err != nil {
		tigin.AbortWithError(c, errs.NewBadRequest("invalid url", nil))
		return
	}
	if uriParams.OrgID == 0 {
		tigin.AbortWithError(c, errs.NewBadRequest("org_id is invalid", nil))
		return
	}
	var payload types.QueryAuditLogFilter
	if err := c.ShouldBindQuery(&payload); err != nil {
		tigin.AbortWithError(c, errs.NewBadRequest(err.Error(), err))
		return
	}

	// overwrite filters
	payload.OrgID = uriParams.OrgID

	events, count, err := auditSvc.QueryEvents(ctx, payload)
	if err != nil {
		tigin.AbortWithError(c, err)
		return
	}

	var exportEvents []types.ExportEvent
	for _, e := range events {
		exportEvents = append(exportEvents, dto.FormatEvent(e))
	}

	resp := types.QueryAuditLogResp{
		Events:     exportEvents,
		TotalCount: count,
	}
	c.JSON(http.StatusOK, resp)
}

func HandleOpsExportAuditLog(c *gin.Context) {
	ctx := c.Request.Context()
	uriParams := tigin.URIParams{}
	if err := c.ShouldBindUri(&uriParams); err != nil {
		tigin.AbortWithError(c, errs.NewBadRequest("invalid url", nil))
		return
	}
	if uriParams.OrgID == 0 {
		tigin.AbortWithError(c, errs.NewBadRequest("org_id is invalid", nil))
		return
	}
	var payload types.ExportAuditLogReq
	if err := c.ShouldBindQuery(&payload); err != nil {
		tigin.AbortWithError(c, errs.NewBadRequest(err.Error(), err))
		return
	}
	if !(payload.Format == consts.ExportFormatJSON || payload.Format == consts.ExportFormatCSV) {
		tigin.AbortWithError(c, errs.NewBadRequest("invalid export format", nil))
		return
	}

	// overwrite filters
	payload.OrgID = uriParams.OrgID

	events, err := auditSvc.ExportEvents(ctx, payload)
	if err != nil {
		tigin.AbortWithError(c, err)
		return
	}

	var reader *bytes.Reader

	if payload.Format == consts.ExportFormatJSON {
		var exportEvents []types.ExportEvent
		for _, e := range events {
			exportEvents = append(exportEvents, dto.FormatEvent(e))
		}

		resp := types.ExportAuditLogResp{Records: exportEvents}
		b, err := json.Marshal(&resp)
		if err != nil {
			tigin.AbortWithError(c, err)
			return
		}

		reader = bytes.NewReader(b)
	} else {
		var b []byte
		buf := bytes.NewBuffer(b)
		csvWriter := csv.NewWriter(buf)
		header := []string{
			"type", "ends_at", "operator_type", "operator_id", "operator_email", "operator_ip",
			"operator_login_method", "org_id", "org_name", "project_id", "project_name",
			"cluster_id", "cluster_name", "trace_id", "result", "details",
		}
		csvWriter.Write(header) //nolint:errcheck

		for _, event := range events {
			var (
				operatorID string
				orgID      string
				projectID  string
				clusterID  string
				traceID    string
			)
			if event.OperatorID > 0 {
				operatorID = fmt.Sprintf("%d", event.OperatorID)
			}
			if event.OrgID > 0 {
				orgID = fmt.Sprintf("%d", event.OrgID)
			}
			if event.ProjectID > 0 {
				projectID = fmt.Sprintf("%d", event.ProjectID)
			}
			if event.ClusterID > 0 {
				clusterID = fmt.Sprintf("%d", event.ClusterID)
			}
			if event.TraceID != "" {
				traceID = event.TraceID
			} else {
				traceID = "-"
			}

			record := []string{
				consts.GetEventTypeName(event.Type),
				event.EndsAt.Format(consts.ExportDateTimeFormat),
				consts.GetOperatorTypeName(event.OperatorType),
				operatorID,
				event.OperatorEmail,
				event.OperatorIP,
				event.OperatorLoginMethod,
				orgID,
				event.OrgName,
				projectID,
				event.ProjectName,
				clusterID,
				event.ClusterName,
				traceID,
				consts.GetResultName(event.Result),
				event.Details,
			}
			if err = csvWriter.Write(record); err != nil {
				tilog.Warn(ctx, "failed to write record to csv", zap.Error(err))
			}
		}

		csvWriter.Flush()
		reader = bytes.NewReader(buf.Bytes())
	}

	now := time.Now().Unix()
	filename := fmt.Sprintf("audit_%d.%s", now, payload.Format)
	contentDescription := fmt.Sprintf(`attachment; filename="%s"`, filename)

	contentLength := int64(reader.Len())
	contentType := "application/octet-stream"
	extraHeaders := map[string]string{
		"Content-Disposition": contentDescription,
	}

	c.DataFromReader(http.StatusOK, contentLength, contentType, reader, extraHeaders)
}

func HandleExportAuditLog(c *gin.Context) {
	ctx := c.Request.Context()
	uriParams, ok := checkIfHavePermission(c)
	if !ok {
		tilog.Error(ctx, "current user is not owner, reject to service")
		return
	}

	var payload types.ExportAuditLogReq
	if err := c.ShouldBindQuery(&payload); err != nil {
		tigin.AbortWithError(c, errs.NewBadRequest(err.Error(), err))
		return
	}
	if !(payload.Format == consts.ExportFormatJSON || payload.Format == consts.ExportFormatCSV) {
		tigin.AbortWithError(c, errs.NewBadRequest("invalid export format", nil))
		return
	}

	// overwrite filters
	payload.OrgID = uriParams.OrgID

	events, err := auditSvc.ExportEvents(ctx, payload)
	if err != nil {
		tigin.AbortWithError(c, err)
		return
	}

	var reader *bytes.Reader

	if payload.Format == consts.ExportFormatJSON {
		var exportEvents []types.ExportEvent
		for _, e := range events {
			exportEvents = append(exportEvents, dto.FormatEvent(e))
		}

		resp := types.ExportAuditLogResp{Records: exportEvents}
		b, err := json.Marshal(&resp)
		if err != nil {
			tigin.AbortWithError(c, err)
			return
		}

		reader = bytes.NewReader(b)
	} else {
		var b []byte
		buf := bytes.NewBuffer(b)
		csvWriter := csv.NewWriter(buf)
		header := []string{
			"type", "ends_at", "operator_type", "operator_id", "operator_email", "operator_ip",
			"operator_login_method", "org_id", "org_name", "project_id", "project_name",
			"cluster_id", "cluster_name", "trace_id", "result", "details",
		}
		csvWriter.Write(header) //nolint:errcheck

		for _, event := range events {
			var (
				operatorID string
				orgID      string
				projectID  string
				clusterID  string
				traceID    string
			)
			if event.OperatorID > 0 {
				operatorID = fmt.Sprintf("%d", event.OperatorID)
			}
			if event.OrgID > 0 {
				orgID = fmt.Sprintf("%d", event.OrgID)
			}
			if event.ProjectID > 0 {
				projectID = fmt.Sprintf("%d", event.ProjectID)
			}
			if event.ClusterID > 0 {
				clusterID = fmt.Sprintf("%d", event.ClusterID)
			}
			if event.TraceID != "" {
				traceID = event.TraceID
			} else {
				traceID = "-"
			}

			record := []string{
				consts.GetEventTypeName(event.Type),
				event.EndsAt.Format(consts.ExportDateTimeFormat),
				consts.GetOperatorTypeName(event.OperatorType),
				operatorID,
				event.OperatorEmail,
				event.OperatorIP,
				event.OperatorLoginMethod,
				orgID,
				event.OrgName,
				projectID,
				event.ProjectName,
				clusterID,
				event.ClusterName,
				traceID,
				consts.GetResultName(event.Result),
				event.Details,
			}
			if err = csvWriter.Write(record); err != nil {
				tilog.Warn(ctx, "failed to write record to csv", zap.Error(err))
			}
		}

		csvWriter.Flush()
		reader = bytes.NewReader(buf.Bytes())
	}

	now := time.Now().Unix()
	filename := fmt.Sprintf("audit_%d.%s", now, payload.Format)
	contentDescription := fmt.Sprintf(`attachment; filename="%s"`, filename)

	contentLength := int64(reader.Len())
	contentType := "application/octet-stream"
	extraHeaders := map[string]string{
		"Content-Disposition": contentDescription,
	}

	c.DataFromReader(http.StatusOK, contentLength, contentType, reader, extraHeaders)
}
