package postgres

import (
	"context"
	"database/sql"
	"strconv"

	"code.justin.tv/eventbus/controlplane/internal/auditlog"
	db "code.justin.tv/eventbus/controlplane/internal/db"

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

const (
	ServicesTableName = "services"

	createServiceQuery = `insert into services (service_catalog_id, name, ldap_group, description) 
values (:service_catalog_id, :name, :ldap_group, :description) 
returning id
`

	selectAllServicesQuery = `select * from services 
order by name
`
	selectServiceByIDQuery = `select * from services 
where id = $1
`

	selectServiceByServiceCatalogIDQuery = `select * from services 
where service_catalog_id = $1
`

	selectServiceByLDAPGroupQuery = `select * from services 
where ldap_group = $1 
order by name
`
	deleteServiceQuery = `delete from services 
where id = $1
`
)

func (pg *PostgresDB) ServiceCreate(ctx context.Context, service *db.Service) (int, error) {
	var tx *sqlx.Tx
	var err error

	tx, err = pg.newTx()
	if err != nil {
		return -1, errors.Wrap(err, "could not begin transaction")
	}

	defer func() {
		_ = pg.cancelTx(tx, err)
	}()

	rows, err := tx.NamedQuery(createServiceQuery, service)
	if err != nil {
		return -1, errors.Wrap(pgError(err, ServicesTableName), "could not insert service into db")
	}

	var id int
	rows.Next()
	err = rows.Scan(&id)
	if err != nil {
		return -1, errors.Wrap(err, "could not retrieve inserted service id")
	}
	rows.Close()

	err = tx.Commit()
	if err != nil {
		return -1, errors.Wrap(err, "could not commit transaction")
	}

	return id, nil
}

func (pg *PostgresDB) ServiceUpdate(ctx context.Context, id int, serviceEditable *db.ServiceEditable) (int, error) {
	var tx *sqlx.Tx
	var err error

	tx, err = pg.newTx()
	if err != nil {
		return -1, errors.Wrap(err, "could not begin transaction")
	}

	defer func() {
		_ = pg.cancelTx(tx, err)
	}()
	_, err = pg.serviceUpdateTx(ctx, tx, id, serviceEditable)
	if err != nil {
		return id, err
	}

	err = tx.Commit()
	if err != nil {
		return -1, errors.Wrap(err, "could not commit transaction")
	}
	return id, nil
}

func (pg *PostgresDB) serviceUpdateTx(ctx context.Context, tx *sqlx.Tx, id int, serviceEditable *db.ServiceEditable) (bool, error) {
	var origService db.Service
	err := tx.GetContext(ctx, &origService, selectServiceByIDQuery, id)
	if err == sql.ErrNoRows {
		return false, db.ErrResourceNotFound
	}

	strID := strconv.Itoa(id)

	if origService.ServiceCatalogID != serviceEditable.ServiceCatalogID ||
		origService.Name != serviceEditable.Name ||
		origService.LDAPGroup != serviceEditable.LDAPGroup ||
		origService.Description != serviceEditable.Description {
		updateServiceQuery := "update services set service_catalog_id = :service_catalog_id, name = :name, ldap_group = :ldap_group, description = :description where id = " + strID
		updateRes, err := tx.NamedExec(updateServiceQuery, *serviceEditable)

		base := &auditlog.BaseLog{
			ResourceName: serviceEditable.Name,
			ServiceID:    id,
			DB:           pg,
		}
		base.LogServiceUpdate(ctx, tx, err, &origService, serviceEditable)
		if err != nil {
			return false, errors.Wrap(err, "could not update service "+strID)
		}

		numRowsAffected, err := updateRes.RowsAffected()
		if err != nil {
			return false, errors.Wrap(err, "could not determine rows affected")
		}
		if numRowsAffected == 0 {
			return false, db.ErrServiceNotFound
		}
		return true, nil
	}

	return false, nil
}

func (pg *PostgresDB) ServiceDelete(ctx context.Context, id int) error {
	deleteRes, err := pg.writer.ExecContext(ctx, deleteServiceQuery, id)
	if err != nil {
		return err
	}
	numRowsAffected, err := deleteRes.RowsAffected()
	if err != nil {
		return err
	}
	if numRowsAffected == 0 {
		return db.ErrServiceNotFound
	}
	return nil
}

func (pg *PostgresDB) Services(ctx context.Context) ([]*db.Service, error) {
	var services []*db.Service
	err := pg.reader.SelectContext(ctx, &services, selectAllServicesQuery)
	return services, err
}

func (pg *PostgresDB) ServiceByID(ctx context.Context, id int) (*db.Service, error) {
	var service db.Service
	err := pg.reader.GetContext(ctx, &service, selectServiceByIDQuery, id)
	if err != nil {
		return nil, pgError(err, ServicesTableName)
	}
	return &service, nil
}

func (pg *PostgresDB) ServiceByServiceCatalogID(ctx context.Context, id string) (*db.Service, error) {
	var service db.Service
	err := pg.reader.GetContext(ctx, &service, selectServiceByServiceCatalogIDQuery, id)
	if err == sql.ErrNoRows {
		return nil, db.ErrResourceNotFound
	}
	return &service, err
}

func (pg *PostgresDB) ServicesByLDAPGroup(ctx context.Context, ldap string) ([]*db.Service, error) {
	var services []*db.Service
	err := pg.reader.SelectContext(ctx, &services, selectServiceByLDAPGroupQuery, ldap)
	return services, err
}

func (pg *PostgresDB) ServicesWithIAMRoles(ctx context.Context) ([]*db.Service, error) {
	services, err := pg.Services(ctx)
	if err != nil {
		return nil, errors.Wrap(err, "could not select all services")
	}

	iamRoles, err := pg.IAMRoles(ctx)
	if err != nil {
		return nil, errors.Wrap(err, "could not select all IAM roles")
	}

	iamRolesByServiceID := make(map[int][]*db.IAMRole)
	for _, iamRole := range iamRoles {
		iamRolesByServiceID[iamRole.ServiceID] = append(iamRolesByServiceID[iamRole.ServiceID], iamRole)
	}

	for _, service := range services {
		service.IAMRoles = iamRolesByServiceID[service.ID]
	}

	return services, nil
}
