package assets

import (
	"bytes"
	"context"
	"crypto/sha1"
	"errors"
	"fmt"
	"regexp"

	"git.handmade.network/hmn/hmn/src/config"
	"git.handmade.network/hmn/hmn/src/db"
	"git.handmade.network/hmn/hmn/src/models"
	"git.handmade.network/hmn/hmn/src/oops"
	"github.com/aws/aws-sdk-go-v2/aws"
	awsconfig "github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/credentials"
	"github.com/aws/aws-sdk-go-v2/service/s3"
	"github.com/aws/aws-sdk-go-v2/service/s3/types"
	"github.com/aws/smithy-go"
	"github.com/google/uuid"
)

var client *s3.Client

func init() {
	cfg, err := awsconfig.LoadDefaultConfig(context.Background(),
		awsconfig.WithCredentialsProvider(
			credentials.NewStaticCredentialsProvider(
				config.Config.DigitalOcean.AssetsSpacesKey,
				config.Config.DigitalOcean.AssetsSpacesSecret,
				"",
			),
		),
		awsconfig.WithRegion(config.Config.DigitalOcean.AssetsSpacesRegion),
		awsconfig.WithEndpointResolver(aws.EndpointResolverFunc(func(service, region string) (aws.Endpoint, error) {
			return aws.Endpoint{
				URL: config.Config.DigitalOcean.AssetsSpacesEndpoint,
			}, nil
		})),
	)
	if err != nil {
		panic(err)
	}
	client = s3.NewFromConfig(cfg, func(o *s3.Options) {
		o.UsePathStyle = true
	})
}

type CreateInput struct {
	Content     []byte
	Filename    string
	ContentType string

	// Optional params
	UploaderID    *int // HMN user id
	Width, Height int
}

var REIllegalFilenameChars = regexp.MustCompile(`[^\w\-.]`)

func SanitizeFilename(filename string) string {
	if filename == "" {
		return "unnamed"
	}
	return REIllegalFilenameChars.ReplaceAllString(filename, "_")
}

func AssetKey(id, filename string) string {
	return fmt.Sprintf("%s/%s", id, filename)
}

type InvalidAssetError error

func Create(ctx context.Context, dbConn db.ConnOrTx, in CreateInput) (*models.Asset, error) {
	filename := SanitizeFilename(in.Filename)

	if len(in.Content) == 0 {
		return nil, InvalidAssetError(fmt.Errorf("could not upload asset '%s': no bytes of data were provided", filename))
	}
	if in.ContentType == "" {
		return nil, InvalidAssetError(fmt.Errorf("could not upload asset '%s': no content type provided", filename))
	}

	// Upload the asset to the DO space
	id := uuid.New()
	key := AssetKey(id.String(), filename)
	checksum := fmt.Sprintf("%x", sha1.Sum(in.Content))

	upload := func() error {
		_, err := client.PutObject(ctx, &s3.PutObjectInput{
			Bucket:      &config.Config.DigitalOcean.AssetsSpacesBucket,
			Key:         &key,
			Body:        bytes.NewReader(in.Content),
			ACL:         types.ObjectCannedACLPublicRead,
			ContentType: &in.ContentType,
		})
		return err
	}

	err := upload()
	if err != nil {
		var apiError smithy.APIError
		if errors.As(err, &apiError) && apiError.ErrorCode() == "NoSuchBucket" {
			_, err := client.CreateBucket(ctx, &s3.CreateBucketInput{
				Bucket: &config.Config.DigitalOcean.AssetsSpacesBucket,
			})
			if err != nil {
				return nil, oops.New(err, "failed to create assets bucket")
			}

			err = upload()
			if err != nil {
				return nil, oops.New(err, "failed to upload asset")
			}
		} else {
			return nil, oops.New(err, "failed to upload asset")
		}
	}

	// Save a record in our database
	// TODO(db): Would be convient to use RETURNING here...
	_, err = dbConn.Exec(ctx,
		`
		INSERT INTO asset (id, s3_key, filename, size, mime_type, sha1sum, width, height, uploader_id)
		VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
		`,
		id,
		key,
		filename,
		len(in.Content),
		in.ContentType,
		checksum,
		in.Width,
		in.Height,
		in.UploaderID,
	)
	if err != nil {
		return nil, oops.New(err, "failed to save asset record")
	}

	// Fetch and return the new record
	asset, err := db.QueryOne[models.Asset](ctx, dbConn,
		`
		SELECT $columns
		FROM asset
		WHERE id = $1
		`,
		id,
	)
	if err != nil {
		return nil, oops.New(err, "failed to fetch newly-created asset")
	}

	return asset, nil
}
