package postgres

import (
	"context"
	"fmt"

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

	// Registers which driver to use with database/sql
	_ "github.com/lib/pq"
)

type ConnectionConfig struct {
	Username string
	Password string
	Hostname string
	Dbname   string
	Sslmode  string
}

type Config struct {
	Reader ConnectionConfig
	Writer ConnectionConfig
}

type PostgresDB struct {
	writer *sqlx.DB
	reader *sqlx.DB
}

func connectionString(cfg ConnectionConfig) string {
	return fmt.Sprintf("postgres://%s:%s@%s:5432/%s?sslmode=%s", cfg.Username, cfg.Password, cfg.Hostname, cfg.Dbname, cfg.Sslmode)
}

func New(cfg Config) (*PostgresDB, error) {
	writerConnString := connectionString(cfg.Writer)

	writerDB, err := sqlx.Connect("postgres", writerConnString)
	if err != nil {
		return nil, errors.Wrap(err, "could not connect to writer db")
	}

	readerConnString := connectionString(cfg.Reader)
	readerDB := writerDB
	if readerConnString != writerConnString {
		readerDB, err = sqlx.Connect("postgres", readerConnString)
		if err != nil {
			return nil, errors.Wrap(err, "could not connect to reader")
		}
	}

	return &PostgresDB{
		writer: writerDB,
		reader: readerDB,
	}, nil

}

// We use these functions to abstract away this logic if we choose to make it more complex down the road
func (pg *PostgresDB) newTx() (*sqlx.Tx, error) {
	return pg.writer.Beginx()
}

func (pg *PostgresDB) doneTx(tx *sqlx.Tx) error {
	return tx.Commit()
}

// cancelTx does a best-effort rollback, then returns the original error
func (pg *PostgresDB) cancelTx(tx *sqlx.Tx, err error) error {
	if tx == nil || err == nil {
		return nil
	}

	_ = tx.Rollback()
	return err
}

func (pg *PostgresDB) WriterConn() *sqlx.DB {
	return pg.writer
}

func (pg *PostgresDB) EventTypeAndStreamsCreate(ctx context.Context, eventType *db.EventType, environments []string) ([]*db.EventStream, error) {
	tx, err := pg.newTx()
	if err != nil {
		return nil, errors.Wrap(err, "could not begin db transaction")
	}

	id, err := pg.eventTypeCreateTx(ctx, tx, eventType)
	if err != nil {
		return nil, pg.cancelTx(tx, err)
	}
	eventType.ID = id

	eventStreams := make([]*db.EventStream, len(environments))
	for i, env := range environments {
		eventStream := &db.EventStream{
			Environment: env,
			EventTypeID: id,
			SNSDetails: db.SNSDetails{
				SNSTopicARN: "",
			},
		}
		eventStreamID, err := pg.eventStreamCreateTx(ctx, tx, eventStream)
		if err != nil {
			return nil, pg.cancelTx(tx, err)
		}
		eventStream.ID = eventStreamID
		eventStream.EventType = *eventType
		eventStreams[i] = eventStream
	}
	return eventStreams, pg.doneTx(tx)
}

// EventStreamPublisherAccounts returns the list of accounts that are allowed to publish to the given event stream
// Operates by doing a join through the publications table
func (pg *PostgresDB) EventStreamPublisherAccounts(ctx context.Context, eventStreamID int) ([]*db.Account, error) {
	query := `
		SELECT a.*
		FROM accounts AS a, publications AS p, event_streams AS es
		WHERE a.id = p.account_id AND p.event_stream_id = es.id AND es.id = $1;
	`
	var accounts []*db.Account
	err := pg.reader.SelectContext(ctx, &accounts, query, eventStreamID)
	return accounts, err
}

// EventStreamPublisherIAMRoles returns the list of IAM roles that are allowed to publish to the given event stream
// Operates by doing a join through the publications table
func (pg *PostgresDB) EventStreamPublisherIAMRoles(ctx context.Context, eventStreamID int) ([]*db.IAMRole, error) {
	query := `
		SELECT i.*
		FROM iam_roles AS i, publications AS p, event_streams AS es
		WHERE i.id = p.iam_role_id AND p.event_stream_id = es.id AND es.id = $1;
	`
	var iamRoles []*db.IAMRole
	err := pg.reader.SelectContext(ctx, &iamRoles, query, eventStreamID)
	return iamRoles, err
}

func (pg *PostgresDB) Close() error {
	if pg.reader != pg.writer {
		err := pg.reader.Close()
		if err != nil {
			return err
		}
	}

	return pg.writer.Close()
}

func (pg *PostgresDB) PublisherServicesByEventStreamID(ctx context.Context, eventStreamID int) ([]*db.Service, error) {
	// TODO: https://jira.twitch.com/browse/ASYNC-794
	// Use LEFT JOINs in this query, after `accounts` have been dropped.
	query := `
		SELECT DISTINCT s.*
			FROM services AS s, publications AS p, accounts as a
			WHERE (p.account_id = a.id AND a.service_id = s.id)
			AND p.event_stream_id = $1
		UNION SELECT DISTINCT s.*
			FROM services AS s, publications AS p, iam_roles as i
			WHERE (p.iam_role_id = i.id AND i.service_id = s.id)
			AND p.event_stream_id = $1;
	`
	var services []*db.Service
	err := pg.reader.SelectContext(ctx, &services, query, eventStreamID)
	return services, err
}

func (pg *PostgresDB) ServicesSubscribedToEventType(ctx context.Context, eventType string) ([]*db.Service, error) {
	query := `
select
  distinct svc.*
  from event_types et
  inner join event_streams es
  on et.id = es.event_type_id
  inner join subscriptions s
  on es.id = s.event_stream_id
  inner join subscription_targets t
  on s.subscription_target_id = t.id
  inner join services svc
  on t.service_id = svc.id
  where et.name=$1;
`

	var services []*db.Service
	err := pg.reader.SelectContext(ctx, &services, query, eventType)
	return services, err
}
