//  Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package database

import (
	"context"
	"fmt"
	"time"

	"github.com/harness/gitness/app/api/request"
	"github.com/harness/gitness/registry/app/store"
	"github.com/harness/gitness/registry/app/store/database/util"
	"github.com/harness/gitness/registry/types"
	store2 "github.com/harness/gitness/store"
	"github.com/harness/gitness/store/database"
	"github.com/harness/gitness/store/database/dbtx"

	"github.com/jmoiron/sqlx"
	"github.com/opencontainers/go-digest"
	errors2 "github.com/pkg/errors"
)

type blobDao struct {
	db *sqlx.DB

	//FIXME: Arvind: Move this to controller layer later
	mtRepository store.MediaTypesRepository
}

func NewBlobDao(db *sqlx.DB, mtRepository store.MediaTypesRepository) store.BlobRepository {
	return &blobDao{
		db:           db,
		mtRepository: mtRepository,
	}
}

var (
	PrimaryQuery = database.Builder.Select("blobs.blob_id as blob_id", "blob_media_type_id", "mt_media_type",
		"blob_digest", "blob_size", "blob_created_at", "blob_root_parent_id").
		From("blobs").
		Join("media_types ON mt_id = blobs.blob_media_type_id")
)

type blobDB struct {
	ID           int64  `db:"blob_id"`
	RootParentID int64  `db:"blob_root_parent_id"`
	Digest       []byte `db:"blob_digest"`
	MediaTypeID  int64  `db:"blob_media_type_id"`
	Size         int64  `db:"blob_size"`
	CreatedAt    int64  `db:"blob_created_at"`
	CreatedBy    int64  `db:"blob_created_by"`
}

type blobMetadataDB struct {
	blobDB
	MediaType string `db:"mt_media_type"`
}

func (bd blobDao) FindByDigestAndRootParentID(ctx context.Context, d digest.Digest,
	rootParentID int64) (*types.Blob, error) {
	dgst, err := types.NewDigest(d)
	if err != nil {
		return nil, err
	}

	digestBytes, err := util.GetHexDecodedBytes(string(dgst))
	if err != nil {
		return nil, err
	}

	stmt := PrimaryQuery.
		Where("blob_root_parent_id = ?", rootParentID).
		Where("blob_digest = ?", digestBytes)

	db := dbtx.GetAccessor(ctx, bd.db)

	dst := new(blobMetadataDB)
	sql, args, err := stmt.ToSql()
	if err != nil {
		return nil, errors2.Wrap(err, "Failed to convert query to sql")
	}

	if err = db.GetContext(ctx, dst, sql, args...); err != nil {
		return nil, database.ProcessSQLErrorf(ctx, err, "Failed to find blob")
	}

	return bd.mapToBlob(dst)
}

func (bd blobDao) FindByID(ctx context.Context, id int64) (*types.Blob, error) {
	stmt := PrimaryQuery.
		Where("blob_id = ?", id)

	db := dbtx.GetAccessor(ctx, bd.db)

	dst := new(blobMetadataDB)
	sql, args, err := stmt.ToSql()
	if err != nil {
		return nil, errors2.Wrap(err, "Failed to convert query to sql")
	}

	if err = db.GetContext(ctx, dst, sql, args...); err != nil {
		return nil, database.ProcessSQLErrorf(ctx, err, "Failed to find blob")
	}

	return bd.mapToBlob(dst)
}

func (bd blobDao) FindByDigestAndRepoID(ctx context.Context, d digest.Digest, repoID int64,
	imageName string) (*types.Blob, error) {
	dgst, err := types.NewDigest(d)
	if err != nil {
		return nil, err
	}

	digestBytes, err := util.GetHexDecodedBytes(string(dgst))
	if err != nil {
		return nil, err
	}

	stmt := PrimaryQuery.
		Join("registry_blobs ON rblob_blob_id = blobs.blob_id").
		Where("rblob_registry_id = ?", repoID).
		Where("rblob_image_name = ?", imageName).
		Where("blob_digest = ?", digestBytes)

	db := dbtx.GetAccessor(ctx, bd.db)

	dst := new(blobMetadataDB)
	sql, args, err := stmt.ToSql()
	if err != nil {
		return nil, errors2.Wrap(err, "Failed to convert query to sql")
	}

	if err = db.GetContext(ctx, dst, sql, args...); err != nil {
		return nil, database.ProcessSQLErrorf(ctx, err, "Failed to find blob")
	}

	return bd.mapToBlob(dst)
}

func (bd blobDao) CreateOrFind(ctx context.Context, b *types.Blob) (*types.Blob, error) {
	sqlQuery := `INSERT INTO blobs (
                   blob_digest, 
                   blob_root_parent_id, 
                   blob_media_type_id, 
                   blob_size,
                   blob_created_at,
                   blob_created_by
        ) VALUES (
                  :blob_digest, 
                  :blob_root_parent_id,
                  :blob_media_type_id,
                  :blob_size,
                  :blob_created_at,
                  :blob_created_by
        ) ON CONFLICT (
            blob_digest, blob_root_parent_id
        ) DO NOTHING 
          RETURNING blob_id`

	mediaTypeID, err := bd.mtRepository.MapMediaType(ctx, b.MediaType)
	if err != nil {
		return nil, err
	}
	b.MediaTypeID = mediaTypeID

	db := dbtx.GetAccessor(ctx, bd.db)
	blob, err := mapToInternalBlob(ctx, b)
	if err != nil {
		return nil, err
	}
	query, arg, err := db.BindNamed(sqlQuery, blob)
	if err != nil {
		return nil, database.ProcessSQLErrorf(ctx, err, "Failed to bind repo object")
	}

	if err = db.QueryRowContext(ctx, query, arg...).Scan(&b.ID); err != nil {
		err := database.ProcessSQLErrorf(ctx, err, "Insert query failed")
		if !errors2.Is(err, store2.ErrResourceNotFound) {
			return nil, err
		}
	}

	return bd.FindByDigestAndRootParentID(ctx, b.Digest, b.RootParentID)
}

func (bd blobDao) DeleteByID(ctx context.Context, id int64) error {
	stmt := database.Builder.Delete("blobs").
		Where("blob_id = ?", id)

	sql, args, err := stmt.ToSql()
	if err != nil {
		return fmt.Errorf("failed to convert purge blob query to sql: %w", err)
	}

	db := dbtx.GetAccessor(ctx, bd.db)

	_, err = db.ExecContext(ctx, sql, args...)
	if err != nil {
		return database.ProcessSQLErrorf(ctx, err, "the delete query failed")
	}

	return nil
}

func (bd blobDao) ExistsBlob(ctx context.Context, repoID int64,
	d digest.Digest, image string) (bool, error) {
	stmt := database.Builder.Select("EXISTS (SELECT 1 FROM registry_blobs " +
		"JOIN blobs as b ON rblob_blob_id = b.blob_id " +
		"WHERE rblob_registry_id = ? AND " +
		"rblob_image_name = ? AND " +
		"b.blob_digest = ?)")

	sql, args, err := stmt.ToSql()
	if err != nil {
		return false, fmt.Errorf("failed to convert exists blob query to sql: %w", err)
	}

	var exists bool
	db := dbtx.GetAccessor(ctx, bd.db)
	newDigest, err := types.NewDigest(d)
	if err != nil {
		return false, err
	}
	bytes, err := util.GetHexDecodedBytes(string(newDigest))
	if err != nil {
		return false, err
	}
	args = append(args, repoID, image, bytes)

	if err = db.GetContext(ctx, &exists, sql, args...); err != nil {
		return false, database.ProcessSQLErrorf(ctx, err, "Failed to check exists blob")
	}

	return exists, nil
}

func mapToInternalBlob(ctx context.Context, in *types.Blob) (*blobDB, error) {
	session, _ := request.AuthSessionFrom(ctx)
	if in.CreatedAt.IsZero() {
		in.CreatedAt = time.Now()
	}
	if in.CreatedBy == 0 {
		in.CreatedBy = session.Principal.ID
	}

	in.CreatedBy = -1
	newDigest, err := types.NewDigest(in.Digest)
	if err != nil {
		return nil, err
	}

	digestBytes, err := util.GetHexDecodedBytes(string(newDigest))
	if err != nil {
		return nil, err
	}

	return &blobDB{
		ID:           in.ID,
		RootParentID: in.RootParentID,
		MediaTypeID:  in.MediaTypeID,
		Digest:       digestBytes,
		Size:         in.Size,
		CreatedAt:    in.CreatedAt.UnixMilli(),
		CreatedBy:    in.CreatedBy,
	}, nil
}

func (bd blobDao) mapToBlob(dst *blobMetadataDB) (*types.Blob, error) {
	createdBy := int64(-1)
	dig := types.Digest(util.GetHexEncodedString(dst.Digest))
	parsedDigest, err := dig.Parse()
	if err != nil {
		return nil, err
	}
	return &types.Blob{
		ID:           dst.ID,
		RootParentID: dst.RootParentID,
		MediaTypeID:  dst.MediaTypeID,
		MediaType:    dst.MediaType,
		Digest:       parsedDigest,
		Size:         dst.Size,
		CreatedAt:    time.UnixMilli(dst.CreatedAt),
		CreatedBy:    createdBy,
	}, nil
}
