package pgdump

import (
	"context"
	"fmt"
	"io"
	"os"
	"os/exec"
	"regexp"
	"strings"

	"git.tswf.io/infra/go-synapse-backupper/pkg/domain/pgdump"
)

var (
	passwordPattern = regexp.MustCompile(`(?i)\bpassword=[^\s]*`)
	hostPattern     = regexp.MustCompile(`(?i)\bhost=[^\s]*`)
	// excludeTablePattern accepts unquoted PostgreSQL identifiers or the
	// schema.table form. It rejects shell-special characters and injection
	// payloads while still allowing the default Synapse table name.
	excludeTablePattern = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_$]*(\.[a-zA-Z_][a-zA-Z0-9_$]*)?$`)
)

// adapter provides pg_dump functionality using the system's pg_dump binary.
type adapter struct {
	commandContext func(ctx context.Context, name string, arg ...string) *exec.Cmd
}

// New creates a new pg_dump adapter.
func New() pgdump.Dumper {
	return &adapter{
		commandContext: exec.CommandContext,
	}
}

// Dump executes pg_dump and writes the output to sink.
//
// The password is never passed through the PGPASSWORD environment variable;
// instead a temporary .pgpass file with 0o600 permissions is created and
// pointed to via PGPASSFILE.
func (a *adapter) Dump(
	ctx context.Context,
	opts pgdump.Options,
	sink io.Writer,
) (retErr error) {
	defer func() {
		if pipeWriter, ok := sink.(*io.PipeWriter); ok {
			if retErr != nil {
				_ = pipeWriter.CloseWithError(retErr)
			} else {
				_ = pipeWriter.Close()
			}
		}
	}()

	args, err := buildArgs(opts)
	if err != nil {
		return err
	}

	cmd := a.commandContext(ctx, "pg_dump", args...)

	env := os.Environ()

	pgpassPath, cleanupPgpass, err := writePgPassFile(opts)
	if err != nil {
		return err
	}
	defer cleanupPgpass()
	if pgpassPath != "" {
		env = append(env, fmt.Sprintf("PGPASSFILE=%s", pgpassPath))
	}

	if opts.Host != "" {
		env = append(env, fmt.Sprintf("PGHOST=%s", opts.Host))
	}
	if opts.Port != 0 {
		env = append(env, fmt.Sprintf("PGPORT=%d", opts.Port))
	}
	if opts.User != "" {
		env = append(env, fmt.Sprintf("PGUSER=%s", opts.User))
	}
	if opts.Database != "" {
		env = append(env, fmt.Sprintf("PGDATABASE=%s", opts.Database))
	}
	cmd.Env = env

	var stderrBuilder strings.Builder
	cmd.Stderr = &stderrBuilder
	cmd.Stdout = sink

	runErr := cmd.Run()
	stderr := sanitizeStderr(strings.TrimSpace(stderrBuilder.String()))

	if runErr != nil {
		if exitErr, ok := runErr.(*exec.ExitError); ok {
			return fmt.Errorf("%w (exit %d): %s", pgdump.ErrPgDumpFailed(exitErr.ExitCode()), exitErr.ExitCode(), stderr)
		}
		return runErr
	}

	if cmd.ProcessState.ExitCode() != 0 {
		return fmt.Errorf("%w (exit %d): %s", pgdump.ErrPgDumpFailed(cmd.ProcessState.ExitCode()), cmd.ProcessState.ExitCode(), stderr)
	}

	return nil
}

// writePgPassFile creates a temporary .pgpass file when a password is provided.
// The returned cleanup function removes the file; callers should defer it.
func writePgPassFile(opts pgdump.Options) (string, func(), error) {
	if opts.Password == "" {
		return "", func() {}, nil
	}

	passFile, err := os.CreateTemp("", "pgpass-*.conf")
	if err != nil {
		return "", nil, fmt.Errorf("create temporary pgpass file: %w", err)
	}
	path := passFile.Name()
	cleanup := func() { _ = os.Remove(path) }

	line := fmt.Sprintf(
		"%s:%d:%s:%s:%s\n",
		opts.Host,
		opts.Port,
		opts.Database,
		opts.User,
		opts.Password,
	)
	if _, err := passFile.WriteString(line); err != nil {
		_ = passFile.Close()
		cleanup()
		return "", nil, fmt.Errorf("write temporary pgpass file: %w", err)
	}
	if err := passFile.Close(); err != nil {
		cleanup()
		return "", nil, fmt.Errorf("close temporary pgpass file: %w", err)
	}
	if err := os.Chmod(path, 0o600); err != nil {
		cleanup()
		return "", nil, fmt.Errorf("chmod temporary pgpass file: %w", err)
	}
	return path, cleanup, nil
}

// sanitizeStderr removes sensitive connection-string fragments from pg_dump
// diagnostics before they are logged.
func sanitizeStderr(input string) string {
	if input == "" {
		return ""
	}
	out := passwordPattern.ReplaceAllString(input, "password=***")
	out = hostPattern.ReplaceAllString(out, "host=***")
	return out
}

func buildArgs(opts pgdump.Options) ([]string, error) {
	excludeTables := opts.ExcludeTables
	if len(excludeTables) == 0 {
		excludeTables = make([]string, 1)
		excludeTables[0] = "e2e_one_time_keys_json"
	}

	args := make([]string, 0, len(excludeTables)+1)
	args = append(args, "--format=custom")

	for _, table := range excludeTables {
		if !excludeTablePattern.MatchString(table) {
			return nil, fmt.Errorf("invalid exclude-table identifier %q", table)
		}
		args = append(args, "--exclude-table="+table)
	}

	return args, nil
}
