package mysql

import (
	"context"
	"database/sql"
	"fmt"
	"strings"

	"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
	"github.com/fleetdm/fleet/v4/server/fleet"
	"github.com/jmoiron/sqlx"
)

func (ds *Datastore) ApplyPackSpecs(ctx context.Context, specs []*fleet.PackSpec) (err error) {
	err = ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
		for _, spec := range specs {
			if err := applyPackSpecDB(ctx, tx, spec); err != nil {
				return ctxerr.Wrapf(ctx, err, "applying pack '%s'", spec.Name)
			}
		}

		return nil
	})

	return err
}

func applyPackSpecDB(ctx context.Context, tx sqlx.ExtContext, spec *fleet.PackSpec) error {
	if spec.Name == "" {
		return ctxerr.New(ctx, "pack name must not be empty")
	}

	// Insert/update pack
	query := `
		INSERT INTO packs (name, description, platform, disabled)
		VALUES (?, ?, ?, ?)
		ON DUPLICATE KEY UPDATE
			name = VALUES(name),
			description = VALUES(description),
			platform = VALUES(platform),
			disabled = VALUES(disabled)
	`
	if _, err := tx.ExecContext(ctx, query, spec.Name, spec.Description, spec.Platform, spec.Disabled); err != nil {
		return ctxerr.Wrap(ctx, err, "insert/update pack")
	}

	// Get Pack ID
	// This is necessary because MySQL last_insert_id does not return a value
	// if no update was made.
	var packID uint
	query = "SELECT id FROM packs WHERE name = ?"
	if err := sqlx.GetContext(ctx, tx, &packID, query, spec.Name); err != nil {
		return ctxerr.Wrap(ctx, err, "getting pack ID")
	}

	// Delete existing scheduled queries for pack
	query = "DELETE FROM scheduled_queries WHERE pack_id = ?"
	if _, err := tx.ExecContext(ctx, query, packID); err != nil {
		return ctxerr.Wrap(ctx, err, "delete existing scheduled queries")
	}

	// Insert new scheduled queries for pack
	query = `
		INSERT INTO scheduled_queries (
			pack_id, query_name, name, description, ` + "`interval`" + `,
			snapshot, removed, shard, platform, version, denylist
		)
		VALUES (
			?, ?, ?, ?, ?,
			?, ?, ?, ?, ?, ?
		)
	`
	for _, q := range spec.Queries {
		// Default to query name if scheduled query name is not specified.
		if q.Name == "" {
			q.Name = q.QueryName
		}
		_, err := tx.ExecContext(ctx, query,
			packID, q.QueryName, q.Name, q.Description, q.Interval,
			q.Snapshot, q.Removed, q.Shard, q.Platform, q.Version, q.Denylist,
		)
		switch {
		case isChildForeignKeyError(err):
			return ctxerr.Errorf(ctx, "cannot schedule unknown query '%s'", q.QueryName)
		case err != nil:
			return ctxerr.Wrapf(ctx, err, "adding query %s referencing %s", q.Name, q.QueryName)
		}
	}

	// Delete existing targets
	query = "DELETE FROM pack_targets WHERE pack_id = ?"
	if _, err := tx.ExecContext(ctx, query, packID); err != nil {
		return ctxerr.Wrap(ctx, err, "delete existing targets")
	}

	query = `
		INSERT INTO pack_targets (pack_id, type, target_id)
		VALUES (?, ?, (SELECT id FROM labels WHERE name = ?))
	`
	for _, l := range spec.Targets.Labels {
		if _, err := tx.ExecContext(ctx, query, packID, fleet.TargetLabel, l); err != nil {
			return ctxerr.Wrap(ctx, err, "adding label to pack")
		}
	}

	query = `
		INSERT INTO pack_targets (pack_id, type, target_id)
		VALUES (?, ?, (SELECT id FROM teams WHERE name = ?))
	`
	for _, t := range spec.Targets.Teams {
		if _, err := tx.ExecContext(ctx, query, packID, fleet.TargetTeam, t); err != nil {
			return ctxerr.Wrap(ctx, err, "adding team to pack")
		}
	}

	return nil
}

func (ds *Datastore) GetPackSpecs(ctx context.Context) ([]*fleet.PackSpec, error) {
	var specs []*fleet.PackSpec
	err := ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
		// Get basic specs
		query := "SELECT id, name, description, platform, disabled FROM packs WHERE pack_type IS NULL OR pack_type = ''"
		if err := sqlx.SelectContext(ctx, tx, &specs, query); err != nil {
			return ctxerr.Wrap(ctx, err, "get packs")
		}

		// Load targets
		for _, spec := range specs {

			// Load labels
			query = `
SELECT l.name
FROM labels l JOIN pack_targets pt
WHERE pack_id = ? AND pt.type = ? AND pt.target_id = l.id
`
			if err := sqlx.SelectContext(ctx, tx, &spec.Targets.Labels, query, spec.ID, fleet.TargetLabel); err != nil {
				return ctxerr.Wrap(ctx, err, "get pack label targets")
			}

			// Load teams
			query = `
SELECT t.name
FROM teams t JOIN pack_targets pt
WHERE pack_id = ? AND pt.type = ? AND pt.target_id = t.id
`
			if err := sqlx.SelectContext(ctx, tx, &spec.Targets.Teams, query, spec.ID, fleet.TargetTeam); err != nil {
				return ctxerr.Wrap(ctx, err, "get pack team targets")
			}
		}

		// Load queries
		for _, spec := range specs {
			query = `
SELECT
query_name, name, description, ` + "`interval`" + `,
snapshot, removed, shard, platform, version, denylist
FROM scheduled_queries
WHERE pack_id = ?
`
			if err := sqlx.SelectContext(ctx, tx, &spec.Queries, query, spec.ID); err != nil {
				return ctxerr.Wrap(ctx, err, "get pack queries")
			}
		}

		return nil
	})
	if err != nil {
		return nil, err
	}

	return specs, nil
}

func (ds *Datastore) GetPackSpec(ctx context.Context, name string) (*fleet.PackSpec, error) {
	spec := &fleet.PackSpec{}
	err := ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
		// Get basic spec
		query := "SELECT id, name, description, platform, disabled FROM packs WHERE name = ?"
		if err := sqlx.GetContext(ctx, tx, spec, query, name); err != nil {
			if err == sql.ErrNoRows {
				return ctxerr.Wrap(ctx, notFound("Pack").WithName(name))
			}
			return ctxerr.Wrap(ctx, err, "get packs")
		}

		// Load label targets
		query = `
SELECT l.name
FROM labels l JOIN pack_targets pt
WHERE pack_id = ? AND pt.type = ? AND pt.target_id = l.id
`
		if err := sqlx.SelectContext(ctx, tx, &spec.Targets.Labels, query, spec.ID, fleet.TargetLabel); err != nil {
			return ctxerr.Wrap(ctx, err, "get pack label targets")
		}

		// Load team targets
		query = `
SELECT t.name
FROM teams t JOIN pack_targets pt
WHERE pack_id = ? AND pt.type = ? AND pt.target_id = t.id
`
		if err := sqlx.SelectContext(ctx, tx, &spec.Targets.Teams, query, spec.ID, fleet.TargetTeam); err != nil {
			return ctxerr.Wrap(ctx, err, "get pack team targets")
		}

		// Load queries
		query = `
SELECT
query_name, name, description, ` + "`interval`" + `,
snapshot, removed, shard, platform, version, denylist
FROM scheduled_queries
WHERE pack_id = ?
`
		if err := sqlx.SelectContext(ctx, tx, &spec.Queries, query, spec.ID); err != nil {
			return ctxerr.Wrap(ctx, err, "get pack queries")
		}

		return nil
	})
	if err != nil {
		return nil, err
	}

	return spec, nil
}

func (ds *Datastore) PackByName(ctx context.Context, name string, opts ...fleet.OptionalArg) (*fleet.Pack, bool, error) {
	sqlStatement := `
		SELECT *
			FROM packs
			WHERE name = ?
	`
	var pack fleet.Pack
	err := sqlx.GetContext(ctx, ds.reader, &pack, sqlStatement, name)
	if err != nil {
		if err == sql.ErrNoRows {
			return nil, false, nil
		}
		return nil, false, ctxerr.Wrap(ctx, err, "fetch pack by name")
	}

	if err := loadPackTargetsDB(ctx, ds.reader, &pack); err != nil {
		return nil, false, err
	}

	return &pack, true, nil
}

// NewPack creates a new Pack
func (ds *Datastore) NewPack(ctx context.Context, pack *fleet.Pack, opts ...fleet.OptionalArg) (*fleet.Pack, error) {
	if err := ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
		query := `
			INSERT INTO packs
			(name, description, platform, disabled)
			VALUES ( ?, ?, ?, ? )
		`
		result, err := tx.ExecContext(ctx, query, pack.Name, pack.Description, pack.Platform, pack.Disabled)
		if err != nil {
			return ctxerr.Wrap(ctx, err, "insert pack")
		}

		id, _ := result.LastInsertId()
		pack.ID = uint(id)

		if err := replacePackTargetsDB(ctx, tx, pack); err != nil {
			return err
		}

		return nil
	}); err != nil {
		return nil, err
	}
	return pack, nil
}

func replacePackTargetsDB(ctx context.Context, tx sqlx.ExecerContext, pack *fleet.Pack) error {
	sql := `DELETE FROM pack_targets WHERE pack_id = ?`
	if _, err := tx.ExecContext(ctx, sql, pack.ID); err != nil {
		return ctxerr.Wrap(ctx, err, "delete pack targets")
	}

	// Insert labels
	if len(pack.LabelIDs) > 0 {
		var args []interface{}
		for _, id := range pack.LabelIDs {
			args = append(args, pack.ID, fleet.TargetLabel, id)
		}
		values := strings.TrimSuffix(
			strings.Repeat("(?,?,?),", len(pack.LabelIDs)),
			",",
		)
		sql = fmt.Sprintf(`
			INSERT INTO pack_targets (pack_id, type, target_id)
			VALUES %s
		`, values)
		if _, err := tx.ExecContext(ctx, sql, args...); err != nil {
			return ctxerr.Wrap(ctx, err, "insert label targets")
		}
	}

	// Insert hosts
	if len(pack.HostIDs) > 0 {
		var args []interface{}
		for _, id := range pack.HostIDs {
			args = append(args, pack.ID, fleet.TargetHost, id)
		}
		values := strings.TrimSuffix(
			strings.Repeat("(?,?,?),", len(pack.HostIDs)),
			",",
		)
		sql = fmt.Sprintf(`
			INSERT INTO pack_targets (pack_id, type, target_id)
			VALUES %s
		`, values)
		if _, err := tx.ExecContext(ctx, sql, args...); err != nil {
			return ctxerr.Wrap(ctx, err, "insert host targets")
		}
	}

	// Insert teams
	if len(pack.TeamIDs) > 0 {
		var args []interface{}
		for _, id := range pack.TeamIDs {
			args = append(args, pack.ID, fleet.TargetTeam, id)
		}
		values := strings.TrimSuffix(
			strings.Repeat("(?,?,?),", len(pack.TeamIDs)),
			",",
		)
		sql = fmt.Sprintf(`
			INSERT INTO pack_targets (pack_id, type, target_id)
			VALUES %s
		`, values)
		if _, err := tx.ExecContext(ctx, sql, args...); err != nil {
			return ctxerr.Wrap(ctx, err, "insert team targets")
		}
	}

	return nil
}

func loadPackTargetsDB(ctx context.Context, q sqlx.QueryerContext, pack *fleet.Pack) error {
	var targets []fleet.Target
	sql := `
	SELECT type, target_id,
		COALESCE(
			CASE
				WHEN type = ? THEN (SELECT hostname FROM hosts WHERE id = target_id)
				WHEN type = ? THEN (SELECT name FROM teams WHERE id = target_id)
				WHEN type = ? THEN (SELECT name FROM labels WHERE id = target_id)
			END
		, '') AS display_text
	FROM pack_targets
	WHERE pack_id = ?`
	if err := sqlx.SelectContext(
		ctx, q, &targets, sql,
		fleet.TargetHost, fleet.TargetTeam, fleet.TargetLabel, pack.ID,
	); err != nil {
		return ctxerr.Wrap(ctx, err, "select pack targets")
	}

	pack.HostIDs, pack.LabelIDs, pack.TeamIDs = []uint{}, []uint{}, []uint{}
	pack.Hosts, pack.Labels, pack.Teams = []fleet.Target{}, []fleet.Target{}, []fleet.Target{}
	for _, target := range targets {
		switch target.Type {
		case fleet.TargetHost:
			pack.HostIDs = append(pack.HostIDs, target.TargetID)
			pack.Hosts = append(pack.Hosts, target)
		case fleet.TargetLabel:
			pack.LabelIDs = append(pack.LabelIDs, target.TargetID)
			pack.Labels = append(pack.Labels, target)
		case fleet.TargetTeam:
			pack.TeamIDs = append(pack.TeamIDs, target.TargetID)
			pack.Teams = append(pack.Teams, target)
		default:
			return ctxerr.Errorf(ctx, "unknown target type: %d", target.Type)
		}
	}

	return nil
}

// SavePack stores changes to pack
func (ds *Datastore) SavePack(ctx context.Context, pack *fleet.Pack) error {
	return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
		query := `
			UPDATE packs
			SET name = ?, platform = ?, disabled = ?, description = ?
			WHERE id = ?
	`

		results, err := tx.ExecContext(ctx, query, pack.Name, pack.Platform, pack.Disabled, pack.Description, pack.ID)
		if err != nil {
			return ctxerr.Wrap(ctx, err, "updating pack")
		}
		rowsAffected, err := results.RowsAffected()
		if err != nil {
			return ctxerr.Wrap(ctx, err, "rows affected updating packs")
		}
		if rowsAffected == 0 {
			return ctxerr.Wrap(ctx, notFound("Pack").WithID(pack.ID))
		}

		return replacePackTargetsDB(ctx, tx, pack)
	})
}

// DeletePack deletes a fleet.Pack so that it won't show up in results.
func (ds *Datastore) DeletePack(ctx context.Context, name string) error {
	return ds.deleteEntityByName(ctx, packsTable, name)
}

// Pack fetch fleet.Pack with matching ID
func (ds *Datastore) Pack(ctx context.Context, pid uint) (*fleet.Pack, error) {
	return packDB(ctx, ds.reader, pid)
}

func packDB(ctx context.Context, q sqlx.QueryerContext, pid uint) (*fleet.Pack, error) {
	query := `SELECT * FROM packs WHERE id = ?`
	pack := &fleet.Pack{}
	err := sqlx.GetContext(ctx, q, pack, query, pid)
	if err == sql.ErrNoRows {
		return nil, ctxerr.Wrap(ctx, notFound("Pack").WithID(pid))
	} else if err != nil {
		return nil, ctxerr.Wrap(ctx, err, "get pack")
	}

	if err := loadPackTargetsDB(ctx, q, pack); err != nil {
		return nil, err
	}

	return pack, nil
}

// EnsureGlobalPack gets or inserts a pack with type global
func (ds *Datastore) EnsureGlobalPack(ctx context.Context) (*fleet.Pack, error) {
	pack := &fleet.Pack{}
	err := ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
		// read from primary as we will create the pack if it doesn't exist
		err := sqlx.GetContext(ctx, tx, pack, `SELECT * FROM packs WHERE pack_type = 'global'`)
		if err == sql.ErrNoRows {
			pack, err = insertNewGlobalPackDB(ctx, tx)
			return err
		} else if err != nil {
			return ctxerr.Wrap(ctx, err, "get pack")
		}

		return loadPackTargetsDB(ctx, tx, pack)
	})
	if err != nil {
		return nil, err
	}
	return pack, nil
}

func insertNewGlobalPackDB(ctx context.Context, q sqlx.ExtContext) (*fleet.Pack, error) {
	var packID uint
	res, err := q.ExecContext(ctx,
		`INSERT INTO packs (name, description, platform, pack_type) VALUES ('Global', 'Global pack', '','global')`,
	)
	if err != nil {
		return nil, ctxerr.Wrap(ctx, err, "insert pack")
	}
	packId, err := res.LastInsertId()
	if err != nil {
		return nil, ctxerr.Wrap(ctx, err, "last insert id")
	}
	packID = uint(packId)
	if _, err := q.ExecContext(ctx,
		`INSERT INTO pack_targets (pack_id, type, target_id) VALUES (?, ?, (SELECT id FROM labels WHERE name = ?))`,
		packID, fleet.TargetLabel, "All Hosts",
	); err != nil {
		return nil, ctxerr.Wrap(ctx, err, "adding label to pack")
	}

	return packDB(ctx, q, packID)
}

func (ds *Datastore) EnsureTeamPack(ctx context.Context, teamID uint) (*fleet.Pack, error) {
	pack := &fleet.Pack{}
	err := ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
		t, err := teamDB(ctx, tx, teamID)
		if err != nil || t == nil {
			return ctxerr.Wrap(ctx, err, "Error finding team")
		}

		teamType := fmt.Sprintf("team-%d", teamID)
		// read from primary as we will create the team pack if it doesn't exist
		err = sqlx.GetContext(ctx, tx, pack, `SELECT * FROM packs WHERE pack_type = ?`, teamType)
		if err == sql.ErrNoRows {
			pack, err = insertNewTeamPackDB(ctx, tx, t)
			return err
		} else if err != nil {
			return ctxerr.Wrap(ctx, err, "get pack")
		}

		if err := loadPackTargetsDB(ctx, tx, pack); err != nil {
			return err
		}

		return nil
	})
	if err != nil {
		return nil, err
	}
	return pack, nil
}

func teamScheduleName(team *fleet.Team) string {
	return fmt.Sprintf("Team: %s", team.Name)
}

func teamSchedulePackType(team *fleet.Team) string {
	return teamSchedulePackTypeByID(team.ID)
}

func teamSchedulePackTypeByID(teamID uint) string {
	return fmt.Sprintf("team-%d", teamID)
}

func insertNewTeamPackDB(ctx context.Context, q sqlx.ExtContext, team *fleet.Team) (*fleet.Pack, error) {
	var packID uint
	res, err := q.ExecContext(ctx,
		`INSERT INTO packs (name, description, platform, pack_type)
                   VALUES (?, 'Schedule additional queries for all hosts assigned to this team.', '',?)`,
		teamScheduleName(team), teamSchedulePackType(team),
	)
	if err != nil {
		return nil, ctxerr.Wrap(ctx, err, "insert team pack")
	}
	packId, err := res.LastInsertId()
	if err != nil {
		return nil, ctxerr.Wrap(ctx, err, "last insert id")
	}
	packID = uint(packId)
	if _, err := q.ExecContext(ctx,
		`INSERT INTO pack_targets (pack_id, type, target_id) VALUES (?, ?, ?)`,
		packID, fleet.TargetTeam, team.ID,
	); err != nil {
		return nil, ctxerr.Wrap(ctx, err, "adding team id target to pack")
	}
	return packDB(ctx, q, packID)
}

// ListPacks returns all fleet.Pack records limited and sorted by fleet.ListOptions
func (ds *Datastore) ListPacks(ctx context.Context, opt fleet.PackListOptions) ([]*fleet.Pack, error) {
	query := `SELECT * FROM packs WHERE pack_type IS NULL OR pack_type = ''`
	if opt.IncludeSystemPacks {
		query = `SELECT * FROM packs`
	}
	var packs []*fleet.Pack
	err := sqlx.SelectContext(ctx, ds.reader, &packs, appendListOptionsToSQL(query, opt.ListOptions))
	if err != nil && err != sql.ErrNoRows {
		return nil, ctxerr.Wrap(ctx, err, "listing packs")
	}

	for _, pack := range packs {
		if err := loadPackTargetsDB(ctx, ds.reader, pack); err != nil {
			return nil, err
		}
	}

	return packs, nil
}

func (ds *Datastore) ListPacksForHost(ctx context.Context, hid uint) ([]*fleet.Pack, error) {
	return listPacksForHost(ctx, ds.reader, hid)
}

// listPacksForHost returns all the packs that are configured to run on the given host.
func listPacksForHost(ctx context.Context, db sqlx.QueryerContext, hid uint) ([]*fleet.Pack, error) {
	query := `
SELECT DISTINCT packs.* FROM (
	(
		SELECT p.* FROM packs p
		JOIN pack_targets pt
		JOIN label_membership lm
		ON (
			p.id = pt.pack_id
			AND pt.target_id = lm.label_id
			AND pt.type = ?
		)
		WHERE lm.host_id = ? AND NOT p.disabled
	)
	UNION ALL
	(
		SELECT p.* FROM packs p
		JOIN pack_targets pt
		ON (p.id = pt.pack_id AND pt.type = ? AND pt.target_id = ?)
	)
	UNION ALL
	(
		SELECT p.*
		FROM packs p
		JOIN pack_targets pt
		ON (p.id = pt.pack_id AND pt.type = ? AND pt.target_id = (SELECT team_id FROM hosts WHERE id = ?)))
	) packs`
	packs := []*fleet.Pack{}
	if err := sqlx.SelectContext(ctx, db, &packs, query,
		fleet.TargetLabel, hid, fleet.TargetHost, hid, fleet.TargetTeam, hid,
	); err != nil && err != sql.ErrNoRows {
		return nil, ctxerr.Wrap(ctx, err, "listing hosts in pack")
	}
	return packs, nil
}
