package jdbc

import (
	"context"
	"errors"
	"fmt"
	"net/url"
	"strings"

	logContext "github.com/trufflesecurity/trufflehog/v3/pkg/context"

	"github.com/go-sql-driver/mysql"
)

type MysqlJDBC struct {
	ConnectionInfo
}

var _ JDBC = (*MysqlJDBC)(nil)

func (s *MysqlJDBC) ping(ctx context.Context) pingResult {
	return ping(ctx, "mysql", isMySQLErrorDeterminate,
		buildMySQLConnectionString(s.Host, "", s.User, s.Password, s.Params))
}

func (s *MysqlJDBC) GetDBType() DatabaseType {
	return MySQL
}

func (s *MysqlJDBC) GetConnectionInfo() *ConnectionInfo {
	return &s.ConnectionInfo
}

func (s *MysqlJDBC) BuildConnectionString() string {
	return buildMySQLConnectionString(s.Host, s.Database, s.User, s.Password, s.Params)
}

func buildMySQLConnectionString(host, database, user, password string, params map[string]string) string {
	conn := host + "/" + database
	userPass := user
	if password != "" {
		userPass = userPass + ":" + password
	}
	if userPass != "" {
		conn = userPass + "@" + conn
	}
	if len(params) > 0 {
		var paramList []string
		for k, v := range params {
			paramList = append(paramList, fmt.Sprintf("%s=%s", k, v))
		}
		conn = conn + "?" + strings.Join(paramList, "&")
	}
	return conn
}

func isMySQLErrorDeterminate(err error) bool {
	// MySQL error numbers from https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html
	if mySQLErr, isMySQLErr := err.(*mysql.MySQLError); isMySQLErr {
		switch mySQLErr.Number {
		case 1044:
			// User access denied to a particular database
			return false // "Indeterminate" so that other connection variations will be tried
		case 1045:
			// User access denied
			return true
		}
	}

	return false
}

func parseMySQL(ctx logContext.Context, subname string) (JDBC, error) {
	// expected form: [subprotocol:]//[user:password@]HOST[/DB][?key=val[&key=val]]
	if !strings.HasPrefix(subname, "//") {
		return nil, errors.New("expected host to start with //")
	}

	// need for hostnames that have tcp(host:port) format required by this database driver
	cfg, err := mysql.ParseDSN(strings.TrimPrefix(subname, "//"))
	if err != nil {
		// fall back to URI parsing
		return parseMySQLURI(ctx, subname)
	}

	if cfg.Addr == "" || cfg.Passwd == "" {
		ctx.Logger().WithName("jdbc").
			V(2).
			Info("Skipping invalid MySQL URL - no password or host found")
		return nil, fmt.Errorf("missing host or password in connection string")
	}
	return &MysqlJDBC{
		ConnectionInfo: ConnectionInfo{
			User:     cfg.User,
			Password: cfg.Passwd,
			Host:     fmt.Sprintf("tcp(%s)", cfg.Addr),
			Params:   map[string]string{"timeout": "5s"},
			Database: cfg.DBName,
		},
	}, nil
}

func parseMySQLURI(ctx logContext.Context, subname string) (JDBC, error) {

	// for standard URI format, which is all i've seen for JDBC
	u, err := url.Parse(subname)
	if err != nil {
		return nil, err
	}

	user := "root"
	pass := ""
	if u.User != nil {
		user = u.User.Username()
		pass, _ = u.User.Password()
	}

	if v := u.Query().Get("user"); v != "" {
		user = v
	}
	if v := u.Query().Get("password"); v != "" {
		pass = v
	}

	if u.Host == "" || pass == "" {
		ctx.Logger().WithName("jdbc").
			V(2).
			Info("Skipping invalid MySQL URL - no password or host found")
		return nil, fmt.Errorf("missing host or password in connection string")
	}

	// Parse database name
	dbName := strings.TrimPrefix(u.Path, "/")
	if dbName == "" {
		dbName = "mysql" // default DB
	}

	return &MysqlJDBC{
		ConnectionInfo: ConnectionInfo{
			User:     user,
			Password: pass,
			Host:     fmt.Sprintf("tcp(%s)", u.Host),
			Params:   map[string]string{"timeout": "5s"},
			Database: dbName,
		},
	}, nil

}
