// 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"
	"database/sql"
	"fmt"
	"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/harness/gitness/types/enum"

	"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_identifier,
	connector_description,
	connector_type,
	connector_auth_type,
	connector_created_by,
	connector_space_id,
	connector_last_test_attempt,
	connector_last_test_error_msg,
	connector_last_test_status,
	connector_created,
	connector_updated,
	connector_version,
	connector_address,
	connector_insecure,
	connector_username,
	connector_github_app_installation_id,
	connector_github_app_application_id,
	connector_region,
	connector_password,
	connector_token,
	connector_aws_key,
	connector_aws_secret,
	connector_github_app_private_key,
	connector_token_refresh
	`
)

type connector struct {
	ID               int64  `db:"connector_id"`
	Identifier       string `db:"connector_identifier"`
	Description      string `db:"connector_description"`
	Type             string `db:"connector_type"`
	AuthType         string `db:"connector_auth_type"`
	CreatedBy        int64  `db:"connector_created_by"`
	SpaceID          int64  `db:"connector_space_id"`
	LastTestAttempt  int64  `db:"connector_last_test_attempt"`
	LastTestErrorMsg string `db:"connector_last_test_error_msg"`
	LastTestStatus   string `db:"connector_last_test_status"`
	Created          int64  `db:"connector_created"`
	Updated          int64  `db:"connector_updated"`
	Version          int64  `db:"connector_version"`

	Address                 sql.NullString `db:"connector_address"`
	Insecure                sql.NullBool   `db:"connector_insecure"`
	Username                sql.NullString `db:"connector_username"`
	GithubAppInstallationID sql.NullString `db:"connector_github_app_installation_id"`
	GithubAppApplicationID  sql.NullString `db:"connector_github_app_application_id"`
	Region                  sql.NullString `db:"connector_region"`
	// Password fields are stored as reference to secrets table
	Password            sql.NullInt64 `db:"connector_password"`
	Token               sql.NullInt64 `db:"connector_token"`
	AWSKey              sql.NullInt64 `db:"connector_aws_key"`
	AWSSecret           sql.NullInt64 `db:"connector_aws_secret"`
	GithubAppPrivateKey sql.NullInt64 `db:"connector_github_app_private_key"`
	TokenRefresh        sql.NullInt64 `db:"connector_token_refresh"`
}

// NewConnectorStore returns a new ConnectorStore.
// The secret store is used to resolve the secret references.
func NewConnectorStore(db *sqlx.DB, secretStore store.SecretStore) store.ConnectorStore {
	return &connectorStore{
		db:          db,
		secretStore: secretStore,
	}
}

func (s *connectorStore) mapFromDBConnectors(ctx context.Context, src []*connector) ([]*types.Connector, error) {
	dst := make([]*types.Connector, len(src))
	for i, v := range src {
		m, err := s.mapFromDBConnector(ctx, v)
		if err != nil {
			return nil, fmt.Errorf("could not map from db connector: %w", err)
		}
		dst[i] = m
	}
	return dst, nil
}

func (s *connectorStore) mapToDBConnector(ctx context.Context, v *types.Connector) (*connector, error) {
	to := connector{
		ID:               v.ID,
		Identifier:       v.Identifier,
		Description:      v.Description,
		Type:             v.Type.String(),
		SpaceID:          v.SpaceID,
		CreatedBy:        v.CreatedBy,
		Created:          v.Created,
		Updated:          v.Updated,
		Version:          v.Version,
		LastTestAttempt:  v.LastTestAttempt,
		LastTestErrorMsg: v.LastTestErrorMsg,
		LastTestStatus:   v.LastTestStatus.String(),
	}
	// Parse connector specific configs
	err := s.convertConfigToDB(ctx, v, &to)
	if err != nil {
		return nil, fmt.Errorf("could not convert config to db: %w", err)
	}
	return &to, nil
}

func (s *connectorStore) convertConfigToDB(
	ctx context.Context,
	source *types.Connector,
	to *connector,
) error {
	switch {
	case source.Github != nil:
		to.Address = sql.NullString{String: source.Github.APIURL, Valid: true}
		to.Insecure = sql.NullBool{Bool: source.Github.Insecure, Valid: true}
		if source.Github.Auth == nil {
			return fmt.Errorf("auth is required for github connectors")
		}
		if source.Github.Auth.AuthType != enum.ConnectorAuthTypeBearer {
			return fmt.Errorf("only bearer token auth is supported for github connectors")
		}
		to.AuthType = source.Github.Auth.AuthType.String()
		creds := source.Github.Auth.Bearer
		// use the same space ID as the connector
		tokenID, err := s.secretIdentiferToID(ctx, creds.Token.Identifier, source.SpaceID)
		if err != nil {
			return fmt.Errorf("could not find secret: %w", err)
		}
		to.Token = sql.NullInt64{Int64: tokenID, Valid: true}
	default:
		return fmt.Errorf("no connector config found for type: %s", source.Type)
	}
	return nil
}

// secretIdentiferToID finds the secret ID given the space ID and the identifier.
func (s *connectorStore) secretIdentiferToID(
	ctx context.Context,
	identifier string,
	spaceID int64,
) (int64, error) {
	secret, err := s.secretStore.FindByIdentifier(ctx, spaceID, identifier)
	if err != nil {
		return 0, err
	}
	return secret.ID, nil
}

func (s *connectorStore) mapFromDBConnector(
	ctx context.Context,
	dbConnector *connector,
) (*types.Connector, error) {
	connector := &types.Connector{
		ID:               dbConnector.ID,
		Identifier:       dbConnector.Identifier,
		Description:      dbConnector.Description,
		Type:             enum.ConnectorType(dbConnector.Type),
		SpaceID:          dbConnector.SpaceID,
		CreatedBy:        dbConnector.CreatedBy,
		LastTestAttempt:  dbConnector.LastTestAttempt,
		LastTestErrorMsg: dbConnector.LastTestErrorMsg,
		LastTestStatus:   enum.ConnectorStatus(dbConnector.LastTestStatus),
		Created:          dbConnector.Created,
		Updated:          dbConnector.Updated,
		Version:          dbConnector.Version,
	}
	err := s.populateConnectorData(ctx, dbConnector, connector)
	if err != nil {
		return nil, fmt.Errorf("could not populate connector data: %w", err)
	}
	return connector, nil
}

func (s *connectorStore) populateConnectorData(
	ctx context.Context,
	source *connector,
	to *types.Connector,
) error {
	switch enum.ConnectorType(source.Type) {
	case enum.ConnectorTypeGithub:
		githubData, err := s.parseGithubConnectorData(ctx, source)
		if err != nil {
			return fmt.Errorf("could not parse github connector data: %w", err)
		}
		to.Github = githubData
	// Cases for other connectors can be added here
	default:
		return fmt.Errorf("unsupported connector type: %s", source.Type)
	}
	return nil
}

func (s *connectorStore) parseGithubConnectorData(
	ctx context.Context,
	connector *connector,
) (*types.GithubConnectorData, error) {
	auth, err := s.parseAuthenticationData(ctx, connector)
	if err != nil {
		return nil, fmt.Errorf("could not parse authentication data: %w", err)
	}
	return &types.GithubConnectorData{
		APIURL:   connector.Address.String,
		Insecure: connector.Insecure.Bool,
		Auth:     auth,
	}, nil
}

func (s *connectorStore) parseAuthenticationData(
	ctx context.Context,
	connector *connector,
) (*types.ConnectorAuth, error) {
	authType, err := enum.ParseConnectorAuthType(connector.AuthType)
	if err != nil {
		return nil, err
	}

	switch authType {
	case enum.ConnectorAuthTypeBasic:
		if !connector.Username.Valid || !connector.Password.Valid {
			return nil, fmt.Errorf("basic auth requires both username and password")
		}
		passwordRef, err := s.convertToRef(ctx, connector.Password.Int64)
		if err != nil {
			return nil, fmt.Errorf("could not convert basicauth password to ref: %w", err)
		}
		return &types.ConnectorAuth{
			AuthType: enum.ConnectorAuthTypeBasic,
			Basic: &types.BasicAuthCreds{
				Username: connector.Username.String,
				Password: passwordRef,
			},
		}, nil
	case enum.ConnectorAuthTypeBearer:
		if !connector.Token.Valid {
			return nil, fmt.Errorf("bearer auth requires a token")
		}
		tokenRef, err := s.convertToRef(ctx, connector.Token.Int64)
		if err != nil {
			return nil, fmt.Errorf("could not convert bearer token to ref: %w", err)
		}
		return &types.ConnectorAuth{
			AuthType: enum.ConnectorAuthTypeBearer,
			Bearer: &types.BearerTokenCreds{
				Token: tokenRef,
			},
		}, nil
	default:
		return nil, fmt.Errorf("unsupported auth type: %s", connector.AuthType)
	}
}

func (s *connectorStore) convertToRef(ctx context.Context, id int64) (types.SecretRef, error) {
	secret, err := s.secretStore.Find(ctx, id)
	if err != nil {
		return types.SecretRef{}, err
	}
	return types.SecretRef{
		Identifier: secret.Identifier,
	}, nil
}

type connectorStore struct {
	db          *sqlx.DB
	secretStore store.SecretStore
}

// 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(connector)
	if err := db.GetContext(ctx, dst, findQueryStmt, id); err != nil {
		return nil, database.ProcessSQLErrorf(ctx, err, "Failed to find connector")
	}
	return s.mapFromDBConnector(ctx, dst)
}

// 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_identifier = $2`
	db := dbtx.GetAccessor(ctx, s.db)

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

// Create creates a connector.
func (s *connectorStore) Create(ctx context.Context, connector *types.Connector) error {
	dbConnector, err := s.mapToDBConnector(ctx, connector)
	if err != nil {
		return err
	}
	const connectorInsertStmt = `
	INSERT INTO connectors (
		connector_description
		,connector_type
		,connector_created_by
		,connector_space_id
		,connector_identifier
		,connector_last_test_attempt
		,connector_last_test_error_msg
		,connector_last_test_status
		,connector_created
		,connector_updated
		,connector_version
		,connector_auth_type
		,connector_address
		,connector_insecure
		,connector_username
		,connector_github_app_installation_id
		,connector_github_app_application_id
		,connector_region
		,connector_password
		,connector_token
		,connector_aws_key
		,connector_aws_secret
		,connector_github_app_private_key
		,connector_token_refresh
	) VALUES (
		:connector_description
		,:connector_type
		,:connector_created_by
		,:connector_space_id
		,:connector_identifier
		,:connector_last_test_attempt
		,:connector_last_test_error_msg
		,:connector_last_test_status
		,:connector_created
		,:connector_updated
		,:connector_version
		,:connector_auth_type
		,:connector_address
		,:connector_insecure
		,:connector_username
		,:connector_github_app_installation_id
		,:connector_github_app_application_id
		,:connector_region
		,:connector_password
		,:connector_token
		,:connector_aws_key
		,:connector_aws_secret
		,:connector_github_app_private_key
		,:connector_token_refresh
	) RETURNING connector_id`
	db := dbtx.GetAccessor(ctx, s.db)

	query, arg, err := db.BindNamed(connectorInsertStmt, dbConnector)
	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 {
	conn, err := s.mapToDBConnector(ctx, p)
	if err != nil {
		return err
	}
	const connectorUpdateStmt = `
	UPDATE connectors
	SET
		connector_description = :connector_description
		,connector_identifier = :connector_identifier
		,connector_last_test_attempt = :connector_last_test_attempt
		,connector_last_test_error_msg = :connector_last_test_error_msg
		,connector_last_test_status = :connector_last_test_status
		,connector_updated = :connector_updated
		,connector_version = :connector_version
		,connector_auth_type = :connector_auth_type
		,connector_address = :connector_address
		,connector_insecure = :connector_insecure
		,connector_username = :connector_username
		,connector_github_app_installation_id = :connector_github_app_installation_id
		,connector_github_app_application_id = :connector_github_app_application_id
		,connector_region = :connector_region
		,connector_password = :connector_password
		,connector_token = :connector_token
		,connector_aws_key = :connector_aws_key
		,connector_aws_secret = :connector_aws_secret
		,connector_github_app_private_key = :connector_github_app_private_key
		,connector_token_refresh = :connector_token_refresh
	WHERE connector_id = :connector_id AND connector_version = :connector_version - 1`
	o := *conn

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

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

	query, arg, err := db.BindNamed(connectorUpdateStmt, o)
	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 = o.Version
	p.Updated = o.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(PartialMatch("connector_identifier", 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 := []*connector{}
	if err = db.SelectContext(ctx, &dst, sql, args...); err != nil {
		return nil, database.ProcessSQLErrorf(ctx, err, "Failed executing custom list query")
	}

	return s.mapFromDBConnectors(ctx, dst)
}

// 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_identifier = $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(PartialMatch("connector_identifier", 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
}
