//  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"
	"errors"
	"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"
	databaseg "github.com/harness/gitness/store/database"
	"github.com/harness/gitness/store/database/dbtx"

	"github.com/jmoiron/sqlx"
)

type manifestReferenceDao struct {
	db *sqlx.DB
}

func NewManifestReferenceDao(db *sqlx.DB) store.ManifestReferenceRepository {
	return &manifestReferenceDao{
		db: db,
	}
}

type manifestReferenceDB struct {
	ID         int64 `db:"manifest_ref_id"`
	RegistryID int64 `db:"manifest_ref_registry_id"`
	ParentID   int64 `db:"manifest_ref_parent_id"`
	ChildID    int64 `db:"manifest_ref_child_id"`
	CreatedAt  int64 `db:"manifest_ref_created_at"`
	UpdatedAt  int64 `db:"manifest_ref_updated_at"`
	CreatedBy  int64 `db:"manifest_ref_created_by"`
	UpdatedBy  int64 `db:"manifest_ref_updated_by"`
}

func (dao manifestReferenceDao) AssociateManifest(
	ctx context.Context,
	ml *types.Manifest, m *types.Manifest,
) error {
	if ml.ID == m.ID {
		return fmt.Errorf("cannot associate a manifest with itself")
	}
	const sqlQuery = `
		INSERT INTO manifest_references ( 
			manifest_ref_registry_id
			,manifest_ref_parent_id
			,manifest_ref_child_id
			,manifest_ref_created_at
			,manifest_ref_updated_at
			,manifest_ref_created_by
			,manifest_ref_updated_by
		) VALUES (
			:manifest_ref_registry_id
			,:manifest_ref_parent_id
			,:manifest_ref_child_id
			,:manifest_ref_created_at
			,:manifest_ref_updated_at
			,:manifest_ref_created_by
			,:manifest_ref_updated_by
		) ON CONFLICT (manifest_ref_registry_id, manifest_ref_parent_id, manifest_ref_child_id)
			DO NOTHING
			RETURNING manifest_ref_id`

	manifestRef := &types.ManifestReference{
		RegistryID: ml.RegistryID,
		ParentID:   ml.ID,
		ChildID:    m.ID,
	}

	db := dbtx.GetAccessor(ctx, dao.db)
	query, arg, err := db.BindNamed(sqlQuery, mapToInternalManifestReference(ctx, manifestRef))
	if err != nil {
		return databaseg.ProcessSQLErrorf(ctx, err, "Bind query failed")
	}

	if err = db.QueryRowContext(ctx, query, arg...).Scan(&manifestRef.ID); err != nil {
		err = databaseg.ProcessSQLErrorf(ctx, err, "QueryRowContext failed")
		if errors.Is(err, store2.ErrResourceNotFound) {
			return nil
		}
		if errors.Is(err, store2.ErrForeignKeyViolation) {
			return util.ErrRefManifestNotFound
		}
		return fmt.Errorf("inserting manifest reference: %w", err)
	}
	return nil
}

func (dao manifestReferenceDao) DissociateManifest(
	_ context.Context,
	_ *types.Manifest,
	_ *types.Manifest,
) error {
	// TODO implement me
	panic("implement me")
}

func mapToInternalManifestReference(ctx context.Context, in *types.ManifestReference) *manifestReferenceDB {
	if in.CreatedAt.IsZero() {
		in.CreatedAt = time.Now()
	}
	in.UpdatedAt = time.Now()
	session, _ := request.AuthSessionFrom(ctx)
	if in.CreatedBy == 0 {
		in.CreatedBy = session.Principal.ID
	}
	in.UpdatedBy = session.Principal.ID

	return &manifestReferenceDB{
		ID:         in.ID,
		RegistryID: in.RegistryID,
		ParentID:   in.ParentID,
		ChildID:    in.ChildID,
		CreatedAt:  in.CreatedAt.UnixMilli(),
		UpdatedAt:  in.UpdatedAt.UnixMilli(),
		CreatedBy:  in.CreatedBy,
		UpdatedBy:  in.UpdatedBy,
	}
}
