// 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"
	"strings"
	"time"

	"github.com/harness/gitness/app/store"
	gitness_store "github.com/harness/gitness/store"
	"github.com/harness/gitness/store/database"
	"github.com/harness/gitness/store/database/dbtx"
	"github.com/harness/gitness/types"

	"github.com/jmoiron/sqlx"
	"github.com/pkg/errors"
)

var _ store.ConnectorStore = (*connectorStore)(nil)

const (
	//nolint:goconst
	connectorQueryBase = `
		SELECT` + connectorColumns + `
		FROM connectors`

	connectorColumns = `
	connector_id,
	connector_description,
	connector_space_id,
	connector_uid,
	connector_data,
	connector_created,
	connector_updated,
	connector_version
	`
)

// NewConnectorStore returns a new ConnectorStore.
func NewConnectorStore(db *sqlx.DB) store.ConnectorStore {
	return &connectorStore{
		db: db,
	}
}

type connectorStore struct {
	db *sqlx.DB
}

// Find returns a connector given a connector ID.
func (s *connectorStore) Find(ctx context.Context, id int64) (*types.Connector, error) {
	const findQueryStmt = connectorQueryBase + `
		WHERE connector_id = $1`
	db := dbtx.GetAccessor(ctx, s.db)

	dst := new(types.Connector)
	if err := db.GetContext(ctx, dst, findQueryStmt, id); err != nil {
		return nil, database.ProcessSQLErrorf(ctx, err, "Failed to find connector")
	}
	return dst, nil
}

// FindByIdentifier returns a connector in a given space with a given identifier.
func (s *connectorStore) FindByIdentifier(
	ctx context.Context,
	spaceID int64,
	identifier string,
) (*types.Connector, error) {
	const findQueryStmt = connectorQueryBase + `
		WHERE connector_space_id = $1 AND connector_uid = $2`
	db := dbtx.GetAccessor(ctx, s.db)

	dst := new(types.Connector)
	if err := db.GetContext(ctx, dst, findQueryStmt, spaceID, identifier); err != nil {
		return nil, database.ProcessSQLErrorf(ctx, err, "Failed to find connector")
	}
	return dst, nil
}

// Create creates a connector.
func (s *connectorStore) Create(ctx context.Context, connector *types.Connector) error {
	const connectorInsertStmt = `
	INSERT INTO connectors (
		connector_description
		,connector_type
		,connector_space_id
		,connector_uid
		,connector_data
		,connector_created
		,connector_updated
		,connector_version
	) VALUES (
		:connector_description
		,:connector_type
		,:connector_space_id
		,:connector_uid
		,:connector_data
		,:connector_created
		,:connector_updated
		,:connector_version
	) RETURNING connector_id`
	db := dbtx.GetAccessor(ctx, s.db)

	query, arg, err := db.BindNamed(connectorInsertStmt, connector)
	if err != nil {
		return database.ProcessSQLErrorf(ctx, err, "Failed to bind connector object")
	}

	if err = db.QueryRowContext(ctx, query, arg...).Scan(&connector.ID); err != nil {
		return database.ProcessSQLErrorf(ctx, err, "connector query failed")
	}

	return nil
}

func (s *connectorStore) Update(ctx context.Context, p *types.Connector) error {
	const connectorUpdateStmt = `
	UPDATE connectors
	SET
		connector_description = :connector_description
		,connector_uid = :connector_uid
		,connector_data = :connector_data
		,connector_type = :connector_type
		,connector_updated = :connector_updated
		,connector_version = :connector_version
	WHERE connector_id = :connector_id AND connector_version = :connector_version - 1`
	connector := *p

	connector.Version++
	connector.Updated = time.Now().UnixMilli()

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

	query, arg, err := db.BindNamed(connectorUpdateStmt, connector)
	if err != nil {
		return database.ProcessSQLErrorf(ctx, err, "Failed to bind connector object")
	}

	result, err := db.ExecContext(ctx, query, arg...)
	if err != nil {
		return database.ProcessSQLErrorf(ctx, err, "Failed to update connector")
	}

	count, err := result.RowsAffected()
	if err != nil {
		return database.ProcessSQLErrorf(ctx, err, "Failed to get number of updated rows")
	}

	if count == 0 {
		return gitness_store.ErrVersionConflict
	}

	p.Version = connector.Version
	p.Updated = connector.Updated
	return nil
}

// UpdateOptLock updates the connector using the optimistic locking mechanism.
func (s *connectorStore) UpdateOptLock(ctx context.Context,
	connector *types.Connector,
	mutateFn func(connector *types.Connector) error,
) (*types.Connector, error) {
	for {
		dup := *connector

		err := mutateFn(&dup)
		if err != nil {
			return nil, err
		}

		err = s.Update(ctx, &dup)
		if err == nil {
			return &dup, nil
		}
		if !errors.Is(err, gitness_store.ErrVersionConflict) {
			return nil, err
		}

		connector, err = s.Find(ctx, connector.ID)
		if err != nil {
			return nil, err
		}
	}
}

// List lists all the connectors present in a space.
func (s *connectorStore) List(
	ctx context.Context,
	parentID int64,
	filter types.ListQueryFilter,
) ([]*types.Connector, error) {
	stmt := database.Builder.
		Select(connectorColumns).
		From("connectors").
		Where("connector_space_id = ?", fmt.Sprint(parentID))

	if filter.Query != "" {
		stmt = stmt.Where("LOWER(connector_uid) LIKE ?", fmt.Sprintf("%%%s%%", strings.ToLower(filter.Query)))
	}

	stmt = stmt.Limit(database.Limit(filter.Size))
	stmt = stmt.Offset(database.Offset(filter.Page, filter.Size))

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

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

	dst := []*types.Connector{}
	if err = db.SelectContext(ctx, &dst, sql, args...); err != nil {
		return nil, database.ProcessSQLErrorf(ctx, err, "Failed executing custom list query")
	}

	return dst, nil
}

// Delete deletes a connector given a connector ID.
func (s *connectorStore) Delete(ctx context.Context, id int64) error {
	const connectorDeleteStmt = `
		DELETE FROM connectors
		WHERE connector_id = $1`

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

	if _, err := db.ExecContext(ctx, connectorDeleteStmt, id); err != nil {
		return database.ProcessSQLErrorf(ctx, err, "Could not delete connector")
	}

	return nil
}

// DeleteByIdentifier deletes a connector with a given identifier in a space.
func (s *connectorStore) DeleteByIdentifier(ctx context.Context, spaceID int64, identifier string) error {
	const connectorDeleteStmt = `
	DELETE FROM connectors
	WHERE connector_space_id = $1 AND connector_uid = $2`

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

	if _, err := db.ExecContext(ctx, connectorDeleteStmt, spaceID, identifier); err != nil {
		return database.ProcessSQLErrorf(ctx, err, "Could not delete connector")
	}

	return nil
}

// Count of connectors in a space.
func (s *connectorStore) Count(ctx context.Context, parentID int64, filter types.ListQueryFilter) (int64, error) {
	stmt := database.Builder.
		Select("count(*)").
		From("connectors").
		Where("connector_space_id = ?", parentID)

	if filter.Query != "" {
		stmt = stmt.Where("LOWER(connector_uid) LIKE ?", fmt.Sprintf("%%%s%%", filter.Query))
	}

	sql, args, err := stmt.ToSql()
	if err != nil {
		return 0, errors.Wrap(err, "Failed to convert query to sql")
	}

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

	var count int64
	err = db.QueryRowContext(ctx, sql, args...).Scan(&count)
	if err != nil {
		return 0, database.ProcessSQLErrorf(ctx, err, "Failed executing count query")
	}
	return count, nil
}
