package activity_file

import (
	"context"
	stderrors "errors"
	"fmt"
	"io"
	"net/http"
	"strings"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/aws/aws-sdk-go/service/s3/s3iface"
	"github.com/gin-gonic/gin"
	"github.com/pingcap/errors"
	"go.uber.org/zap"

	"github.com/pingcap-inc/clinic/pkg/activityhub_client"
	apierr "github.com/pingcap-inc/clinic/pkg/api/error"
	clinicsession "github.com/pingcap-inc/clinic/pkg/auth/session"
	"github.com/pingcap-inc/clinic/pkg/config"
	cliniclog "github.com/pingcap-inc/clinic/pkg/log"
)

type activityReader interface {
	GetActivityWithContext(context.Context, string) (*activityhub_client.Activity, error)
}

type DiagnosisActivityAPI struct {
	s3API       s3iface.S3API
	bucket      string
	activityAPI activityReader
}

func NewDiagnosisActivityAPI(cfg *config.Config, bucket, region string) (*DiagnosisActivityAPI, error) {
	if bucket == "" {
		return nil, errors.New("diagnosis activity bucket is empty")
	}
	awsConfig := &aws.Config{
		Region:           aws.String(region),
		S3ForcePathStyle: aws.Bool(true),
	}
	awsSessionOpts := session.Options{
		Config: *awsConfig,
	}
	ses, err := session.NewSessionWithOptions(awsSessionOpts)
	if err != nil {
		return nil, err
	}

	var activityHubAddr, activityHubSource, activityHubKey string
	if cfg != nil && cfg.ActivityManagerConfig != nil {
		activityHubAddr = cfg.ActivityManagerConfig.ActivityHubAddr
		activityHubSource = cfg.ActivityManagerConfig.ActivityHubSource
		activityHubKey = cfg.ActivityManagerConfig.ActivityHubKey
	}

	return &DiagnosisActivityAPI{
		s3API:       s3.New(ses),
		bucket:      bucket,
		activityAPI: activityhub_client.NewClient(activityHubAddr, activityHubSource, activityHubKey),
	}, nil
}

func (api *DiagnosisActivityAPI) DownloadFile(ctx context.Context, key string) (*DownloadFileData, error) {
	input := &s3.GetObjectInput{
		Bucket: aws.String(api.bucket),
		Key:    aws.String(key),
	}
	output, err := api.s3API.GetObjectWithContext(ctx, input)
	if err != nil {
		return nil, err
	}
	s := strings.Split(key, "/")
	fname := s[len(s)-1]
	extraHeaders := map[string]string{
		"Content-Disposition": fmt.Sprintf(`attachment; filename="%s"`, fname),
		"Content-Length":      fmt.Sprintf("%d", *output.ContentLength),
	}
	if output.ETag != nil {
		extraHeaders["ETag"] = *output.ETag
	}
	if output.ContentRange != nil {
		extraHeaders["Content-Range"] = *output.ContentRange
	}
	if output.AcceptRanges != nil {
		extraHeaders["Accept-Range"] = *output.AcceptRanges
	}

	data := &DownloadFileData{
		ContentType:   *output.ContentType,
		ContentLength: *output.ContentLength,
		Reader:        output.Body,
		Headers:       extraHeaders,
	}
	return data, nil
}

func (api *DiagnosisActivityAPI) ViewFile(ctx context.Context, key string) ([]byte, error) {
	input := &s3.GetObjectInput{
		Bucket: aws.String(api.bucket),
		Key:    aws.String(key),
	}
	output, err := api.s3API.GetObjectWithContext(ctx, input)
	if err != nil {
		return nil, err
	}
	defer func() { output.Body.Close() }()
	bs, err := io.ReadAll(output.Body)
	if err != nil {
		return nil, err
	}

	return bs, nil
}

var (
	errActivityFileNotFound = stderrors.New("activity file not found")
	errInvalidActivityFile  = stderrors.New("activity contains an invalid file reference")
)

func (api *DiagnosisActivityAPI) resolveObjectKey(ctx context.Context, param *ActivityFileParam) (string, error) {
	if api.activityAPI == nil {
		return "", errors.New("activity service is unavailable")
	}
	activity, err := api.activityAPI.GetActivityWithContext(ctx, param.ActivityID)
	if err != nil {
		return "", errors.Annotate(err, "get activity")
	}
	if activity == nil ||
		activity.ActivityID != param.ActivityID {
		return "", errActivityFileNotFound
	}

	payload, ok := activity.Payload.(map[string]any)
	if !ok {
		return "", errInvalidActivityFile
	}
	details, ok := payload["details"].(map[string]any)
	if !ok {
		return "", errInvalidActivityFile
	}
	key, ok := details["key"].(string)
	if !ok || validateObjectKey(key) != nil {
		return "", errInvalidActivityFile
	}
	return key, nil
}

func (api *DiagnosisActivityAPI) prepareFileRead(c *gin.Context, operation string) (string, bool) {
	logger := cliniclog.WithContextL(c.Request.Context())
	user := clinicsession.GetUser(c)
	if user == nil || !user.Internal {
		logger.Warn("activity file request rejected",
			zap.String("operation", operation),
			zap.String("stage", "authorization"))
		_ = c.Error(apierr.ErrForbidden.New("permission deny"))
		return "", false
	}
	logger.Debug("activity file request authorized",
		zap.String("operation", operation),
		zap.String("stage", "authorization"))

	param := &ActivityFileParam{
		ActivityID: c.Query("activity_id"),
	}
	if err := param.Validate(); err != nil {
		logger.Warn("activity file request rejected",
			zap.String("operation", operation),
			zap.String("stage", "validation"))
		_ = c.Error(apierr.ErrBadRequest.WrapWithNoMessage(err))
		return "", false
	}
	logger.Debug("activity file request validated",
		zap.String("operation", operation),
		zap.String("stage", "validation"))

	key, err := api.resolveObjectKey(c.Request.Context(), param)
	if err != nil {
		if stderrors.Is(err, errActivityFileNotFound) {
			logger.Warn("activity file request rejected",
				zap.String("operation", operation),
				zap.String("stage", "activity_authorization"))
			_ = c.Error(apierr.ErrNotFound.New("activity file not found"))
			return "", false
		}
		if stderrors.Is(err, errInvalidActivityFile) {
			logger.Warn("activity file request rejected",
				zap.String("operation", operation),
				zap.String("stage", "object_key_validation"))
			_ = c.Error(apierr.ErrBadRequest.New("activity file reference is invalid"))
			return "", false
		}
		logger.Error("activity file lookup failed",
			zap.String("operation", operation),
			zap.String("stage", "activity_lookup"),
			zap.String("error_type", fmt.Sprintf("%T", err)))
		_ = c.Error(apierr.ErrInternal.New("resolve activity file failed"))
		return "", false
	}
	logger.Debug("activity file lookup completed",
		zap.String("operation", operation),
		zap.String("stage", "activity_lookup"))
	return key, true
}

// @Summary		View activity file
// @Description	view activity file
// @Produce		json
// @Param			x-csrf-token	header		string	true	"get value from login.ValidationResp response"
// @Param			activity_id		query		string	true	"ActivityHub activity id"
// @Success		200				{string}	{}
// @Router			/activityhub/file/view [get]
func (api *DiagnosisActivityAPI) ViewFileGinHandler(c *gin.Context) {
	key, ok := api.prepareFileRead(c, "view")
	if !ok {
		return
	}
	bs, err := api.ViewFile(c.Request.Context(), key)
	if err != nil {
		cliniclog.WithContextL(c.Request.Context()).Error("activity file storage read failed",
			zap.String("operation", "view"),
			zap.String("stage", "storage"),
			zap.String("error_type", fmt.Sprintf("%T", err)))
		_ = c.Error(err)
		return
	}
	c.Data(http.StatusOK, "application/json", bs)
	cliniclog.WithContextL(c.Request.Context()).Info("activity file request completed",
		zap.String("operation", "view"),
		zap.String("stage", "success"))
}

// @Summary		Download activity file
// @Description	download activity file
// @Produce		json
// @Param			x-csrf-token	header	string	true	"get value from login.ValidationResp response"
// @Param			activity_id		query	string	true	"ActivityHub activity id"
// @Success		200				{file}	file
// @Router			/activityhub/file/download [get]
func (api *DiagnosisActivityAPI) DownloadFileGinHandler(c *gin.Context) {
	key, ok := api.prepareFileRead(c, "download")
	if !ok {
		return
	}
	data, err := api.DownloadFile(c.Request.Context(), key)
	if err != nil {
		cliniclog.WithContextL(c.Request.Context()).Error("activity file storage read failed",
			zap.String("operation", "download"),
			zap.String("stage", "storage"),
			zap.String("error_type", fmt.Sprintf("%T", err)))
		_ = c.Error(err)
		return
	}
	c.DataFromReader(http.StatusOK, data.ContentLength, data.ContentType, data.Reader, data.Headers)
	_ = data.Close()
	cliniclog.WithContextL(c.Request.Context()).Info("activity file request completed",
		zap.String("operation", "download"),
		zap.String("stage", "success"))
}
