package postgres

import (
	"context"
	"database/sql"
	"time"

	"code.justin.tv/eventbus/controlplane/internal/auditlog"
	"code.justin.tv/eventbus/controlplane/internal/containers"
	db "code.justin.tv/eventbus/controlplane/internal/db"
	"github.com/jmoiron/sqlx"
	"github.com/pkg/errors"
)

const (
	IAMRolesTableName = "iam_roles"

	createIAMRoleQuery = `
INSERT INTO iam_roles (arn, label, service_id, kms_grant_id)
VALUES (:arn, :label, :service_id, :kms_grant_id)
RETURNING id
`

	selectIAMRolesBaseQuery = `
SELECT * FROM iam_roles
`

	selectAllIAMRolesQuery = selectIAMRolesBaseQuery

	selectIAMRolesByServiceIDQuery = selectIAMRolesBaseQuery + `
WHERE service_id = $1
ORDER BY label
`

	updateIAMRoleCloudformationStatusQuery = `
UPDATE iam_roles
SET cloudformation_status = $1
WHERE id = $2
`

	updateIAMRoleLabelQuery = `
UPDATE iam_roles
SET label = $1
WHERE id = $2
`

	selectIAMRoleByARNQuery = selectIAMRolesBaseQuery + `
WHERE arn = $1
`

	selectIAMRoleByIDQuery = selectIAMRolesBaseQuery + `
WHERE id = $1
`

	deleteIAMRoleQuery = `
DELETE FROM iam_roles
WHERE id = $1
`
)

func (pg *PostgresDB) IAMRoleCreate(ctx context.Context, role *db.IAMRole) (int, error) {
	tx, err := pg.newTx()
	if err != nil {
		return -1, errors.Wrap(err, "could not begin transaction")
	}

	id, err := pg.iamRoleCreateTx(ctx, tx, role)
	if err != nil {
		return -1, pg.cancelTx(tx, errors.Wrap(pgError(err, IAMRolesTableName), "could not create IAM role"))
	}

	return id, errors.Wrap(pg.doneTx(tx), "could not commit transaction")
}

func (pg *PostgresDB) iamRoleCreateTx(ctx context.Context, tx *sqlx.Tx, role *db.IAMRole) (int, error) {
	stmt, err := tx.PrepareNamedContext(ctx, createIAMRoleQuery)
	if err != nil {
		return -1, errors.Wrap(err, "could not prepare named statement for `createIAMRoleQuery`")
	}

	var id int
	err = stmt.QueryRowxContext(ctx, role).Scan(&id)

	return id, errors.Wrap(err, "could not create iam role")
}

func (pg *PostgresDB) IAMRoleUpdate(ctx context.Context, id int, iamRoleEditable *db.IAMRoleEditable) (int, error) {
	updateRes, err := pg.writer.ExecContext(ctx, updateIAMRoleLabelQuery, iamRoleEditable.Label, id)
	if err != nil {
		return -1, errors.Wrap(pgError(err, IAMRolesTableName), "could not update iam role")
	}
	numRowsAffected, err := updateRes.RowsAffected()
	if err != nil {
		return -1, errors.Wrap(err, "could not determine rows affected")
	}
	if numRowsAffected == 0 {
		return -1, db.ErrResourceNotFound
	}

	return id, nil
}

func (pg *PostgresDB) IAMRoles(ctx context.Context) ([]*db.IAMRole, error) {
	var iamRoles []*db.IAMRole
	err := pg.reader.SelectContext(ctx, &iamRoles, selectAllIAMRolesQuery)
	return iamRoles, errors.Wrap(err, "could not select all IAM roles")
}

func (pg *PostgresDB) IAMRolesByServiceID(ctx context.Context, serviceID int) ([]*db.IAMRole, error) {
	var iamRoles []*db.IAMRole
	err := pg.reader.SelectContext(ctx, &iamRoles, selectIAMRolesByServiceIDQuery, serviceID)
	return iamRoles, errors.Wrap(err, "could not select IAM roles by service id")
}

func (pg *PostgresDB) IAMRoleUpdateCloudformationStatus(ctx context.Context, id int, cloudformationStatus string) (int, error) {
	updateRes, err := pg.writer.ExecContext(ctx, updateIAMRoleCloudformationStatusQuery, cloudformationStatus, id)
	if err != nil {
		return -1, err
	}

	countUpdated, err := updateRes.RowsAffected()
	if err != nil {
		return -1, errors.Wrap(err, "could not count rows affected")
	}
	if countUpdated == 0 {
		return -1, db.ErrResourceNotFound
	}

	return id, nil
}

func (pg *PostgresDB) IAMRoleByARN(ctx context.Context, arn string) (*db.IAMRole, error) {
	var iamRole db.IAMRole
	err := pg.reader.GetContext(ctx, &iamRole, selectIAMRoleByARNQuery, arn)
	if err == sql.ErrNoRows {
		return nil, pgError(err, IAMRolesTableName)
	}
	return &iamRole, err
}

func (pg *PostgresDB) IAMRoleByID(ctx context.Context, id int) (*db.IAMRole, error) {
	var iamRole db.IAMRole
	err := pg.reader.GetContext(ctx, &iamRole, selectIAMRoleByIDQuery, id)
	if err == sql.ErrNoRows {
		return nil, pgError(err, IAMRolesTableName)
	}
	return &iamRole, err
}

func (pg *PostgresDB) IAMRoleDelete(ctx context.Context, lease db.AWSLease, id int) error {
	if lease == nil {
		return errors.New("nil lease object passed to IAMRoleDelete")
	}

	if lease.Expired() {
		return db.ErrLeaseExpired
	}

	res, err := pg.writer.ExecContext(ctx, deleteIAMRoleQuery, id)
	if err != nil {
		return err
	}

	rowsAffected, err := res.RowsAffected()
	if err != nil {
		return err
	}

	if rowsAffected == 0 {
		return db.ErrResourceNotFound
	}

	return nil
}

func (pg *PostgresDB) IAMRolesEditForService(ctx context.Context, serviceID int, desiredRoles db.IAMRolesEditable) (*db.IAMRolesEditReport, error) {
	tx, err := pg.newTx()
	if err != nil {
		return nil, errors.Wrap(err, "could not begin transaction")
	}

	report, err := pg.iamRolesEditForServiceTx(ctx, tx, serviceID, desiredRoles)
	if err != nil {
		return nil, pg.cancelTx(tx, errors.Wrap(err, "could not update IAM roles for service"))
	}

	return report, errors.Wrap(pg.doneTx(tx), "could not commit transaction")
}

func (pg *PostgresDB) iamRolesEditForServiceTx(ctx context.Context, tx *sqlx.Tx, serviceID int, desiredRoles db.IAMRolesEditable) (*db.IAMRolesEditReport, error) {
	report := &db.IAMRolesEditReport{
		Added:   containers.StringSet{},
		Removed: containers.StringSet{},
	}

	newAudit := func(label string) *auditlog.BaseLog {
		return &auditlog.BaseLog{
			ResourceName: label,
			ServiceID:    serviceID,
			DB:           pg,
		}
	}

	unchecked := desiredRoles.GetARNs()
	var existingRoles []*db.IAMRole
	err := tx.SelectContext(ctx, &existingRoles, selectIAMRolesByServiceIDQuery, serviceID)
	if err != nil {
		return nil, errors.Wrap(err, "could not select existing roles")
	}

	for _, row := range existingRoles {
		if desired := desiredRoles[row.ARN]; desired != nil {
			unchecked.Remove(row.ARN)
			if desired.Label != row.Label {
				_, err = tx.ExecContext(ctx, updateIAMRoleLabelQuery, desired.Label, row.ID)
				newAudit(row.ARN).LogIAMRoleUpdate(ctx, tx, err, row, desired)
				if err != nil {
					return report, errors.Wrap(err, "could not edit IAM role label")
				}
			}
		} else {
			// TODO ASYNC-989 implement proper delete when we support all depending actions
			return report, db.ErrNotSupported
		}
	}

	// anything remaining are brand new entries.
	for roleARN := range unchecked {
		desired := desiredRoles[roleARN]
		report.Added.Add(roleARN)
		role := &db.IAMRole{
			ARN:       roleARN,
			Label:     desired.Label,
			ServiceID: serviceID,
		}
		_, err = pg.iamRoleCreateTx(ctx, tx, role)
		newAudit(roleARN).LogIAMRoleCreateTx(ctx, tx, err, role)
		if err != nil {
			return report, err // could not have added any role here
		}
	}

	return report, nil
}

func (pg *PostgresDB) IAMRoleAcquireLease(ctx context.Context, resourceID int, timeout time.Duration) (db.AWSLease, context.Context, error) {
	return pg.Acquire(ctx, resourceID, IAMRolesTableName, timeout)
}

func (pg *PostgresDB) IAMRoleReleaseLease(lease db.AWSLease) error {
	return lease.Release()
}
