package msdsn

import (
	"bytes"
	"crypto/tls"
	"crypto/x509"
	"encoding/pem"
	"errors"
	"fmt"
	"net"
	"net/url"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"
	"unicode"

	"github.com/google/uuid"
)

type (
	Encryption int
	Log        uint64
	BrowserMsg byte
)

const (
	DsnTypeURL  = 1
	DsnTypeOdbc = 2
	DsnTypeAdo  = 3
)

const (
	EncryptionOff      = 0
	EncryptionRequired = 1
	EncryptionDisabled = 3
	EncryptionStrict   = 4
)

const (
	LogErrors      Log = 1
	LogMessages    Log = 2
	LogRows        Log = 4
	LogSQL         Log = 8
	LogParams      Log = 16
	LogTransaction Log = 32
	LogDebug       Log = 64
	LogRetries     Log = 128
	// LogSessionIDs tells the session logger to include activity id and connection id
	LogSessionIDs Log = 0x8000
)

const (
	BrowserDefault      BrowserMsg = 0
	BrowserAllInstances BrowserMsg = 0x03
	BrowserDAC          BrowserMsg = 0x0f
)

const (
	Database               = "database"
	Encrypt                = "encrypt"
	Password               = "password"
	ChangePassword         = "change password"
	UserID                 = "user id"
	Port                   = "port"
	TrustServerCertificate = "trustservercertificate"
	Certificate            = "certificate"
	ServerCertificate      = "servercertificate"
	TLSMin                 = "tlsmin"
	PacketSize             = "packet size"
	LogParam               = "log"
	ConnectionTimeout      = "connection timeout"
	HostNameInCertificate  = "hostnameincertificate"
	KeepAlive              = "keepalive"
	ServerSpn              = "serverspn"
	WorkstationID          = "workstation id"
	AppName                = "app name"
	ApplicationIntent      = "applicationintent"
	FailoverPartner        = "failoverpartner"
	FailOverPort           = "failoverport"
	FailoverPartnerSpn     = "failoverpartnerspn"
	DisableRetry           = "disableretry"
	Server                 = "server"
	Protocol               = "protocol"
	DialTimeout            = "dial timeout"
	Pipe                   = "pipe"
	MultiSubnetFailover    = "multisubnetfailover"
	NoTraceID              = "notraceid"
	GuidConversion         = "guid conversion"
	Timezone               = "timezone"
	EpaEnabled             = "epa enabled"
)

type EncodeParameters struct {
	// Properly convert GUIDs, using correct byte endianness
	GuidConversion bool
	// Timezone is the timezone to use for encoding and decoding datetime values.
	Timezone *time.Location
}

func (e EncodeParameters) GetTimezone() *time.Location {
	if e.Timezone == nil {
		return time.UTC
	}
	return e.Timezone
}

type Config struct {
	Port       uint64
	Host       string
	Instance   string
	Database   string
	User       string
	Password   string
	Encryption Encryption
	TLSConfig  *tls.Config

	FailOverPartner    string
	FailOverPort       uint64
	FailOverPartnerSPN string

	// If true the TLSConfig servername should use the routed server.
	HostInCertificateProvided bool

	// Read Only intent for application database.
	// NOTE: This does not make queries to most databases read-only.
	ReadOnlyIntent bool

	LogFlags Log

	ServerSPN   string
	Workstation string
	AppName     string

	// If true disables database/sql's automatic retry of queries
	// that start on bad connections.
	DisableRetry bool

	// Do not use the following.

	DialTimeout time.Duration // DialTimeout defaults to 15s per protocol. Set negative to disable.
	ConnTimeout time.Duration // Use context for timeouts.
	KeepAlive   time.Duration // Leave at default.
	PacketSize  uint16

	Parameters map[string]string
	// Protocols is an ordered list of protocols to dial
	Protocols []string
	// ProtocolParameters are written by non-tcp ProtocolParser implementations
	ProtocolParameters map[string]interface{}
	// BrowserMsg is the message identifier to fetch instance data from SQL browser
	BrowserMessage BrowserMsg
	// ChangePassword is used to set the login's password during login. Ignored for non-SQL authentication.
	ChangePassword string
	//ColumnEncryption is true if the application needs to decrypt or encrypt Always Encrypted values
	ColumnEncryption bool
	// Attempt to connect to all IPs in parallel when MultiSubnetFailover is true
	MultiSubnetFailover bool
	// guid to set as Activity Id in the prelogin packet. Defaults to a new value for each Config.
	ActivityID []byte
	// When true, no connection id or trace id value is sent in the prelogin packet.
	// Some cloud servers may block connections that lack such values.
	NoTraceID bool
	// TrustServerCertificate controls whether the client verifies the server certificate.
	// When true, the server certificate is accepted without validation.
	TrustServerCertificate bool
	// Parameters related to type encoding
	Encoding EncodeParameters
	// EPA mode determines how the Channel Bindings are calculated.
	EpaEnabled bool
}

func readDERFile(filename string) ([]byte, error) {
	derBytes, err := os.ReadFile(filename)
	if err != nil {
		return nil, err
	}

	cert, err := x509.ParseCertificate(derBytes)
	if err != nil {
		return nil, err
	}

	pemBytes := pem.EncodeToMemory(&pem.Block{
		Type:  "CERTIFICATE",
		Bytes: cert.Raw,
	})
	return pemBytes, nil
}

func readCertificate(certificate string) ([]byte, error) {
	certType := strings.ToLower(filepath.Ext(certificate))

	switch certType {
	case ".pem":
		return os.ReadFile(certificate)
	case ".der":
		return readDERFile(certificate)
	default:
		return nil, fmt.Errorf("certificate type %s is not supported", certType)
	}
}

// Build a tls.Config object from the supplied certificate.
// serverCertificate is used for byte-comparison validation (skips chain validation and hostname validation)
// certificate is used for traditional chain validation
func SetupTLS(certificate string, serverCertificate string, insecureSkipVerify bool, hostInCertificate string, minTLSVersion string) (*tls.Config, error) {
	config := tls.Config{
		ServerName:         hostInCertificate,
		InsecureSkipVerify: insecureSkipVerify,

		// fix for https://github.com/microsoft/go-mssqldb/issues/166
		// Go implementation of TLS payload size heuristic algorithm splits single TDS package to multiple TCP segments,
		// while SQL Server seems to expect one TCP segment per encrypted TDS package.
		// Setting DynamicRecordSizingDisabled to true disables that algorithm and uses 16384 bytes per TLS package
		DynamicRecordSizingDisabled: true,
		MinVersion:                  TLSVersionFromString(minTLSVersion),
	}

	// Handle serverCertificate parameter (byte-comparison validation)
	if len(serverCertificate) > 0 {
		pem, err := readCertificate(serverCertificate)
		if err != nil {
			return nil, fmt.Errorf("cannot read server certificate %q: %w", serverCertificate, err)
		}
		if err := setupTLSServerCertificateOnly(&config, pem); err != nil {
			return nil, err
		}
		return &config, nil
	}

	// Handle certificate parameter (traditional chain validation)
	if len(certificate) == 0 {
		return &config, nil
	}
	pem, err := readCertificate(certificate)
	if err != nil {
		return nil, fmt.Errorf("cannot read certificate %q: %w", certificate, err)
	}

	if strings.Contains(config.ServerName, ":") && !insecureSkipVerify {
		err := setupTLSCommonName(&config, pem)
		if err != skipSetup {
			return &config, err
		}
	}
	certs := x509.NewCertPool()
	certs.AppendCertsFromPEM(pem)
	config.RootCAs = certs
	return &config, nil
}

// setupTLSServerCertificateOnly validates that the server certificate matches the provided certificate via byte comparison
// This matches the behavior of Microsoft.Data.SqlClient
func setupTLSServerCertificateOnly(config *tls.Config, pemData []byte) error {
	// To match the behavior of Microsoft.Data.SqlClient, we simply compare the raw bytes
	// of the server's certificate with the provided certificate file. This approach:
	// - Does not validate certificate chain, expiry, or subject
	// - Only checks that the server's certificate exactly matches the provided certificate
	// - Skips hostname validation (which is the intended behavior)
	//
	// We use InsecureSkipVerify=true with VerifyPeerCertificate callback because
	// VerifyConnection runs AFTER standard verification (including hostname check).

	// Parse the expected certificate from the PEM data
	block, _ := pem.Decode(pemData)
	if block == nil {
		return errors.New("failed to decode PEM certificate")
	}
	// Store the raw certificate bytes (DER format) for comparison
	expectedCertBytes := block.Bytes

	config.InsecureSkipVerify = true
	config.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
		if len(rawCerts) == 0 {
			return errors.New("no peer certificates provided")
		}

		// Compare the server's certificate bytes with the expected certificate bytes
		// This matches the Microsoft.Data.SqlClient behavior: just compare raw bytes
		serverCertBytes := rawCerts[0]

		if !bytes.Equal(serverCertBytes, expectedCertBytes) {
			return errors.New("server certificate doesn't match the provided certificate")
		}

		return nil
	}
	return nil
}

// parseTLS parses encryption parameters and returns the TLS configuration and trustServerCertificate value.
func parseTLS(params map[string]string, host string) (Encryption, *tls.Config, bool, error) {
	trustServerCert := false

	var encryption Encryption = EncryptionOff
	encrypt, ok := params[Encrypt]
	if ok {
		encrypt = strings.ToLower(encrypt)
		switch encrypt {
		case "mandatory", "yes", "1", "t", "true":
			encryption = EncryptionRequired
		case "disable":
			encryption = EncryptionDisabled
		case "strict":
			encryption = EncryptionStrict
		case "optional", "no", "0", "f", "false":
			encryption = EncryptionOff
		default:
			f := "invalid encrypt '%s'"
			return encryption, nil, false, fmt.Errorf(f, encrypt)
		}
	} else {
		trustServerCert = true
	}
	trust, ok := params[TrustServerCertificate]
	if ok {
		var err error
		trustServerCert, err = strconv.ParseBool(trust)
		if err != nil {
			f := "invalid trust server certificate '%s': %s"
			return encryption, nil, false, fmt.Errorf(f, trust, err.Error())
		}
	}
	certificate := params[Certificate]
	serverCertificate := params[ServerCertificate]
	hostInCertificate := params[HostNameInCertificate]

	// Validate parameter combinations
	if len(serverCertificate) > 0 {
		if len(certificate) > 0 {
			return encryption, nil, false, errors.New("cannot specify both 'certificate' and 'serverCertificate' parameters")
		}
		if len(hostInCertificate) > 0 {
			return encryption, nil, false, errors.New("cannot specify both 'serverCertificate' and 'hostnameincertificate' parameters")
		}
	}

	if encryption != EncryptionDisabled {
		tlsMin := params[TLSMin]
		if encrypt == "strict" {
			trustServerCert = false
		}
		tlsConfig, err := SetupTLS(certificate, serverCertificate, trustServerCert, host, tlsMin)
		if err != nil {
			return encryption, nil, trustServerCert, fmt.Errorf("failed to setup TLS: %w", err)
		}
		return encryption, tlsConfig, trustServerCert, nil
	}
	return encryption, nil, trustServerCert, nil
}

var skipSetup = errors.New("skip setting up TLS")

func getDsnType(dsn string) int {
	if strings.HasPrefix(dsn, "sqlserver://") {
		return DsnTypeURL
	}
	if strings.HasPrefix(dsn, "odbc:") {
		return DsnTypeOdbc
	}
	return DsnTypeAdo
}

func getDsnParams(dsn string) (map[string]string, error) {

	var params map[string]string
	var err error

	switch getDsnType(dsn) {
	case DsnTypeOdbc:
		params, err = splitConnectionStringOdbc(dsn[len("odbc:"):])
		if err != nil {
			return params, err
		}
	case DsnTypeURL:
		params, err = splitConnectionStringURL(dsn)
		if err != nil {
			return params, err
		}
	default:
		params = splitConnectionString(dsn)
	}
	return params, nil
}

func Parse(dsn string) (Config, error) {
	p := Config{
		ProtocolParameters: map[string]interface{}{},
		Protocols:          []string{},
		Encoding: EncodeParameters{
			Timezone: time.UTC,
		},
	}

	activityid, uerr := uuid.NewRandom()
	if uerr == nil {
		p.ActivityID = activityid[:]
	}
	var params map[string]string
	var err error

	params, err = getDsnParams(dsn)
	if err != nil {
		return p, err
	}
	p.Parameters = params

	strlog, ok := params[LogParam]
	if ok {
		flags, err := strconv.ParseUint(strlog, 10, 64)
		if err != nil {
			return p, fmt.Errorf("invalid log parameter '%s': %s", strlog, err.Error())
		}
		p.LogFlags = Log(flags)
	}

	tz, ok := params[Timezone]
	if ok {
		location, err := time.LoadLocation(tz)
		if err != nil {
			return p, fmt.Errorf("invalid timezone '%s': %s", tz, err.Error())
		}
		p.Encoding.Timezone = location
	}

	p.Database = params[Database]
	p.User = params[UserID]
	p.Password = params[Password]
	p.ChangePassword = params[ChangePassword]
	p.Port = 0
	strport, ok := params[Port]
	if ok {
		var err error
		p.Port, err = strconv.ParseUint(strport, 10, 16)
		if err != nil {
			f := "invalid tcp port '%v': %v"
			return p, fmt.Errorf(f, strport, err.Error())
		}
	}

	// https://docs.microsoft.com/en-us/sql/database-engine/configure-windows/configure-the-network-packet-size-server-configuration-option\
	strpsize, ok := params[PacketSize]
	if ok {
		var err error
		psize, err := strconv.ParseUint(strpsize, 0, 16)
		if err != nil {
			f := "invalid packet size '%v': %v"
			return p, fmt.Errorf(f, strpsize, err.Error())
		}

		// Ensure packet size falls within the TDS protocol range of 512 to 32767 bytes
		// NOTE: Encrypted connections have a maximum size of 16383 bytes.  If you request
		// a higher packet size, the server will respond with an ENVCHANGE request to
		// alter the packet size to 16383 bytes.
		p.PacketSize = uint16(psize)
		if p.PacketSize < 512 {
			p.PacketSize = 512
		} else if p.PacketSize > 32767 {
			p.PacketSize = 32767
		}
	}

	// https://msdn.microsoft.com/en-us/library/dd341108.aspx
	//
	// Do not set a connection timeout. Use Context to manage such things.
	// Default to zero, but still allow it to be set.
	if strconntimeout, ok := params[ConnectionTimeout]; ok {
		timeout, err := strconv.ParseUint(strconntimeout, 10, 64)
		if err != nil {
			f := "invalid connection timeout '%v': %v"
			return p, fmt.Errorf(f, strconntimeout, err.Error())
		}
		p.ConnTimeout = time.Duration(timeout) * time.Second
	}

	// default keep alive should be 30 seconds according to spec:
	// https://msdn.microsoft.com/en-us/library/dd341108.aspx
	p.KeepAlive = 30 * time.Second
	if keepAlive, ok := params[KeepAlive]; ok {
		timeout, err := strconv.ParseUint(keepAlive, 10, 64)
		if err != nil {
			f := "invalid keepAlive value '%s': %s"
			return p, fmt.Errorf(f, keepAlive, err.Error())
		}
		p.KeepAlive = time.Duration(timeout) * time.Second
	}

	serverSPN, ok := params[ServerSpn]
	if ok {
		p.ServerSPN = serverSPN
	} // If not set by the app, ServerSPN will be set by the successful dialer.

	workstation, ok := params[WorkstationID]
	if ok {
		p.Workstation = workstation
	} else {
		workstation, err := os.Hostname()
		if err == nil {
			p.Workstation = workstation
		}
	}

	appname, ok := params[AppName]
	if !ok {
		appname = "go-mssqldb"
	}
	p.AppName = appname

	appintent, ok := params[ApplicationIntent]
	if ok {
		if appintent == "ReadOnly" {
			if p.Database == "" {
				return p, fmt.Errorf("database must be specified when ApplicationIntent is ReadOnly")
			}
			p.ReadOnlyIntent = true
		}
	}

	failOverPartner, ok := params[FailoverPartner]
	if ok {
		p.FailOverPartner = failOverPartner
	}

	failOverPort, ok := params[FailOverPort]
	if ok {
		var err error
		p.FailOverPort, err = strconv.ParseUint(failOverPort, 0, 16)
		if err != nil {
			f := "invalid failover port '%v': %v"
			return p, fmt.Errorf(f, failOverPort, err.Error())
		}
	}

	failOverPartnerSPN, ok := params[FailoverPartnerSpn]
	if ok {
		p.FailOverPartnerSPN = failOverPartnerSPN
	}

	disableRetry, ok := params[DisableRetry]
	if ok {
		var err error
		p.DisableRetry, err = strconv.ParseBool(disableRetry)
		if err != nil {
			f := "invalid disableRetry '%s': %s"
			return p, fmt.Errorf(f, disableRetry, err.Error())
		}
	} else {
		p.DisableRetry = disableRetryDefault
	}

	server := params[Server]
	protocol, ok := params[Protocol]

	for _, parser := range ProtocolParsers {
		if (!ok && !parser.Hidden()) || parser.Protocol() == protocol {
			err = parser.ParseServer(server, &p)
			if err != nil {
				// if the caller only wants this protocol , fail right away
				if ok {
					return p, err
				}
			} else {
				// Only enable a protocol if it can handle the server name
				p.Protocols = append(p.Protocols, parser.Protocol())
			}

		}
	}
	if ok && len(p.Protocols) == 0 {
		return p, fmt.Errorf("No protocol handler is available for protocol: '%s'", protocol)
	}

	f := len(p.Protocols)
	if f == 0 {
		f = 1
	}
	p.DialTimeout = time.Duration(15*f) * time.Second
	if strdialtimeout, ok := params[DialTimeout]; ok {
		timeout, err := strconv.ParseUint(strdialtimeout, 10, 64)
		if err != nil {
			f := "invalid dial timeout '%v': %v"
			return p, fmt.Errorf(f, strdialtimeout, err.Error())
		}

		p.DialTimeout = time.Duration(timeout) * time.Second
	}

	hostInCertificate, ok := params[HostNameInCertificate]
	if ok {
		p.HostInCertificateProvided = true
	} else {
		hostInCertificate = p.Host
		p.HostInCertificateProvided = false
	}

	p.Encryption, p.TLSConfig, p.TrustServerCertificate, err = parseTLS(params, hostInCertificate)
	if err != nil {
		return p, err
	}

	if c, ok := params["columnencryption"]; ok {
		columnEncryption, err := strconv.ParseBool(c)
		if err != nil {
			if strings.EqualFold(c, "Enabled") {
				columnEncryption = true
			} else if strings.EqualFold(c, "Disabled") {
				columnEncryption = false
			} else {
				return p, fmt.Errorf("invalid columnencryption '%v' : %v", columnEncryption, err.Error())
			}
		}
		p.ColumnEncryption = columnEncryption
	}

	msf, ok := params[MultiSubnetFailover]
	if ok {
		multiSubnetFailover, err := strconv.ParseBool(msf)
		if err != nil {
			if strings.EqualFold(msf, "Enabled") {
				multiSubnetFailover = true
			} else if strings.EqualFold(msf, "Disabled") {
				multiSubnetFailover = false
			} else {
				return p, fmt.Errorf("invalid multiSubnetFailover value '%v': %v", multiSubnetFailover, err.Error())
			}
		}
		p.MultiSubnetFailover = multiSubnetFailover
	} else {
		// Defaulting to true to prevent breaking change although other client libraries default to false
		p.MultiSubnetFailover = true
	}
	nti, ok := params[NoTraceID]
	if ok {
		notraceid, err := strconv.ParseBool(nti)
		if err == nil {
			p.NoTraceID = notraceid
		}
	}

	guidConversion, ok := params[GuidConversion]
	if ok {
		var err error
		p.Encoding.GuidConversion, err = strconv.ParseBool(guidConversion)
		if err != nil {
			f := "invalid guid conversion '%s': %s"
			return p, fmt.Errorf(f, guidConversion, err.Error())
		}
	} else {
		// set to false for backward compatibility
		p.Encoding.GuidConversion = false
	}

	p.EpaEnabled = false
	epaString, ok := params[EpaEnabled]
	if !ok {
		epaString = os.Getenv("MSSQL_USE_EPA")
	}
	if epaString != "" {
		epaEnabled, err := strconv.ParseBool(epaString)
		if err != nil {
			return p, fmt.Errorf("invalid epa enabled value '%s': %v", epaString, err)
		}
		p.EpaEnabled = epaEnabled
	}

	return p, nil
}

// convert connectionParams to url style connection string
// used mostly for testing
func (p Config) URL() *url.URL {
	q := url.Values{}
	if p.Database != "" {
		q.Add(Database, p.Database)
	}
	if p.LogFlags != 0 {
		q.Add(LogParam, strconv.FormatUint(uint64(p.LogFlags), 10))
	}
	host := p.Host
	protocol := ""
	// Can't just check for a : because of IPv6 host names
	if strings.HasPrefix(host, "admin") || strings.HasPrefix(host, "np") || strings.HasPrefix(host, "sm") || strings.HasPrefix(host, "tcp") {
		hostParts := strings.SplitN(p.Host, ":", 2)
		if len(hostParts) > 1 {
			host = hostParts[1]
			protocol = hostParts[0]
		}
	}
	if p.Port > 0 {
		// Use net.JoinHostPort to properly handle IPv6 addresses (e.g., [::1]:1433)
		host = net.JoinHostPort(host, strconv.Itoa(int(p.Port)))
	}
	q.Add(DisableRetry, fmt.Sprintf("%t", p.DisableRetry))
	protocolParam, ok := p.Parameters[Protocol]
	if ok {
		if protocol != "" && protocolParam != protocol {
			panic("Mismatched protocol parameters!")
		}
		protocol = protocolParam
	}
	if protocol != "" {
		q.Add(Protocol, protocol)
	}
	pipe, ok := p.Parameters[Pipe]
	if ok {
		q.Add(Pipe, pipe)
	}
	res := url.URL{
		Scheme: "sqlserver",
		Host:   host,
		User:   url.UserPassword(p.User, p.Password),
	}
	if p.Instance != "" {
		res.Path = p.Instance
	}
	q.Add(DialTimeout, strconv.FormatFloat(float64(p.DialTimeout.Seconds()), 'f', 0, 64))

	switch p.Encryption {
	case EncryptionDisabled:
		q.Add(Encrypt, "DISABLE")
	case EncryptionRequired:
		q.Add(Encrypt, "true")
	}
	// Only include TrustServerCertificate if it was explicitly set in the original connection string
	if _, ok := p.Parameters[TrustServerCertificate]; ok {
		q.Add(TrustServerCertificate, strconv.FormatBool(p.TrustServerCertificate))
	}
	if p.ColumnEncryption {
		q.Add("columnencryption", "true")
	}

	if p.Encoding.GuidConversion {
		q.Add(GuidConversion, strconv.FormatBool(p.Encoding.GuidConversion))
	}

	if tz := p.Encoding.Timezone; tz != nil && tz != time.UTC {
		q.Add(Timezone, tz.String())
	}

	if p.FailOverPartner != "" {
		q.Add(FailoverPartner, p.FailOverPartner)
	}
	if p.FailOverPort != 0 {
		q.Add(FailOverPort, strconv.FormatUint(p.FailOverPort, 10))
	}
	if p.ServerSPN != "" {
		q.Add(ServerSpn, p.ServerSPN)
	}
	if p.FailOverPartnerSPN != "" {
		q.Add(FailoverPartnerSpn, p.FailOverPartnerSPN)
	}

	if len(q) > 0 {
		res.RawQuery = q.Encode()
	}

	return &res
}

// ADO connection string keywords at https://github.com/dotnet/SqlClient/blob/main/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/DbConnectionStringCommon.cs
var adoSynonyms = map[string]string{
	"application name":          AppName,
	"data source":               Server,
	"address":                   Server,
	"network address":           Server,
	"addr":                      Server,
	"user":                      UserID,
	"uid":                       UserID,
	"pwd":                       Password,
	"initial catalog":           Database,
	"column encryption setting": "columnencryption",
}

func splitConnectionString(dsn string) (res map[string]string) {
	res = map[string]string{}
	parts := splitAdoConnectionStringParts(dsn)
	for _, part := range parts {
		if len(part) == 0 {
			continue
		}
		lst := strings.SplitN(part, "=", 2)
		name := strings.TrimSpace(strings.ToLower(lst[0]))
		if len(name) == 0 {
			continue
		}
		var value string = ""
		if len(lst) > 1 {
			value = strings.TrimSpace(lst[1])
			// Remove surrounding double quotes if present
			if len(value) >= 2 && strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"") {
				value = value[1 : len(value)-1]
				// Unescape double quotes
				value = strings.ReplaceAll(value, "\"\"", "\"")
			}
		}
		synonym, hasSynonym := adoSynonyms[name]
		if hasSynonym {
			name = synonym
		}
		// "server" in ADO can include a protocol and a port.
		if name == Server {
			for _, parser := range ProtocolParsers {
				prot := parser.Protocol() + ":"
				if strings.HasPrefix(value, prot) {
					res[Protocol] = parser.Protocol()
				}
				value = strings.TrimPrefix(value, prot)
			}
			serverParts := strings.Split(value, ",")
			if len(serverParts) == 2 && len(serverParts[1]) > 0 {
				value = serverParts[0]
				res[Port] = serverParts[1]
			}
		}
		res[name] = value
	}
	return res
}

// splitAdoConnectionStringParts splits an ADO connection string into parts,
// properly handling double-quoted values that may contain semicolons
func splitAdoConnectionStringParts(dsn string) []string {
	var parts []string
	var current strings.Builder
	inQuotes := false

	runes := []rune(dsn)
	for i := 0; i < len(runes); i++ {
		char := runes[i]

		if char == '"' {
			if inQuotes && i+1 < len(runes) && runes[i+1] == '"' {
				// Double quote escape sequence - add both quotes to current part
				current.WriteRune(char)
				current.WriteRune(runes[i+1])
				i++ // Skip the next quote
			} else {
				// Start or end of quoted section
				inQuotes = !inQuotes
				current.WriteRune(char)
			}
		} else if char == ';' && !inQuotes {
			// Semicolon outside of quotes - end current part
			parts = append(parts, current.String())
			current.Reset()
		} else {
			current.WriteRune(char)
		}
	}

	// Add the last part if it's not empty
	if current.Len() > 0 {
		parts = append(parts, current.String())
	}

	return parts
}

// Splits a URL of the form sqlserver://username:password@host/instance?param1=value&param2=value
func splitConnectionStringURL(dsn string) (map[string]string, error) {
	res := map[string]string{}

	u, err := url.Parse(dsn)
	if err != nil {
		// Do not include the original error which may contain credentials
		return res, fmt.Errorf("unable to parse connection string: invalid URL format")
	}

	if u.Scheme != "sqlserver" {
		return res, fmt.Errorf("scheme %s is not recognized", u.Scheme)
	}

	if u.User != nil {
		res[UserID] = u.User.Username()
		p, exists := u.User.Password()
		if exists {
			res[Password] = p
		}
	}

	host, port, err := net.SplitHostPort(u.Host)
	if err != nil {
		host = u.Host
	}

	if len(u.Path) > 0 {
		res[Server] = host + "\\" + u.Path[1:]
	} else {
		res[Server] = host
	}

	if len(port) > 0 {
		res[Port] = port
	}

	query := u.Query()
	for k, v := range query {
		if len(v) > 1 {
			return res, fmt.Errorf("key %s provided more than once", k)
		}
		lk := strings.ToLower(k)
		if _, exists := res[lk]; exists {
			return res, fmt.Errorf("key %q provided more than once (connection string keys are case-insensitive; remove the duplicate)", k)
		}
		res[lk] = v[0]
	}

	return res, nil
}

// Splits a URL in the ODBC format
func splitConnectionStringOdbc(dsn string) (map[string]string, error) {
	res := map[string]string{}

	type parserState int
	const (
		// Before the start of a key
		parserStateBeforeKey parserState = iota

		// Inside a key
		parserStateKey

		// Beginning of a value. May be bare or braced
		parserStateBeginValue

		// Inside a bare value
		parserStateBareValue

		// Inside a braced value
		parserStateBracedValue

		// A closing brace inside a braced value.
		// May be the end of the value or an escaped closing brace, depending on the next character
		parserStateBracedValueClosingBrace

		// After a value. Next character should be a semicolon or whitespace.
		parserStateEndValue
	)

	var state = parserStateBeforeKey

	var key string
	var value string

	for i, c := range dsn {
		switch state {
		case parserStateBeforeKey:
			switch {
			case c == '=':
				return res, fmt.Errorf("unexpected character = at index %d. Expected start of key or semi-colon or whitespace", i)
			case !unicode.IsSpace(c) && c != ';':
				state = parserStateKey
				key += string(c)
			}

		case parserStateKey:
			switch c {
			case '=':
				key = normalizeOdbcKey(key)
				state = parserStateBeginValue

			case ';':
				// Key without value
				key = normalizeOdbcKey(key)
				res[key] = value
				key = ""
				value = ""
				state = parserStateBeforeKey

			default:
				key += string(c)
			}

		case parserStateBeginValue:
			switch {
			case c == '{':
				state = parserStateBracedValue
			case c == ';':
				// Empty value
				res[key] = value
				key = ""
				state = parserStateBeforeKey
			case unicode.IsSpace(c):
				// Ignore whitespace
			default:
				state = parserStateBareValue
				value += string(c)
			}

		case parserStateBareValue:
			if c == ';' {
				res[key] = strings.TrimRightFunc(value, unicode.IsSpace)
				key = ""
				value = ""
				state = parserStateBeforeKey
			} else {
				value += string(c)
			}

		case parserStateBracedValue:
			if c == '}' {
				state = parserStateBracedValueClosingBrace
			} else {
				value += string(c)
			}

		case parserStateBracedValueClosingBrace:
			if c == '}' {
				// Escaped closing brace
				value += string(c)
				state = parserStateBracedValue
				continue
			}

			// End of braced value
			res[key] = value
			key = ""
			value = ""

			// This character is the first character past the end,
			// so it needs to be parsed like the parserStateEndValue state.
			state = parserStateEndValue
			switch {
			case c == ';':
				state = parserStateBeforeKey
			case unicode.IsSpace(c):
				// Ignore whitespace
			default:
				return res, fmt.Errorf("unexpected character %c at index %d. Expected semi-colon or whitespace", c, i)
			}

		case parserStateEndValue:
			switch {
			case c == ';':
				state = parserStateBeforeKey
			case unicode.IsSpace(c):
				// Ignore whitespace
			default:
				return res, fmt.Errorf("unexpected character %c at index %d. Expected semi-colon or whitespace", c, i)
			}
		}
	}

	switch state {
	case parserStateBeforeKey: // Okay
	case parserStateKey: // Unfinished key. Treat as key without value.
		key = normalizeOdbcKey(key)
		res[key] = value
	case parserStateBeginValue: // Empty value
		res[key] = value
	case parserStateBareValue:
		res[key] = strings.TrimRightFunc(value, unicode.IsSpace)
	case parserStateBracedValue:
		return res, fmt.Errorf("unexpected end of braced value at index %d", len(dsn))
	case parserStateBracedValueClosingBrace: // End of braced value
		res[key] = value
	case parserStateEndValue: // Okay
	}

	return res, nil
}

// Normalizes the given string as an ODBC-format key
func normalizeOdbcKey(s string) string {
	return strings.ToLower(strings.TrimRightFunc(s, unicode.IsSpace))
}

// ProtocolParser can populate Config with parameters to dial using its protocol
type ProtocolParser interface {
	// ParseServer updates the Config with protocol properties from the server. Returns an error if the server isn't compatible.
	ParseServer(server string, p *Config) error
	// Protocol returns the name of the protocol dialer
	Protocol() string
	// Hidden returns true if this protocol must be explicitly chosen by the application
	Hidden() bool
}

// ProtocolParsers is an ordered list of protocols that can be dialed. Each parser must have a corresponding Dialer in mssql.ProtocolDialers
var ProtocolParsers []ProtocolParser = []ProtocolParser{
	tcpParser{Prefix: "tcp"},
	tcpParser{Prefix: "admin"},
}

type tcpParser struct {
	Prefix string
}

func (t tcpParser) Hidden() bool {
	return t.Prefix == "admin"
}

func (t tcpParser) ParseServer(server string, p *Config) error {
	// a server name can have different forms
	parts := strings.SplitN(server, `\`, 2)
	p.Host = parts[0]
	if p.Host == "." || strings.ToUpper(p.Host) == "(LOCAL)" || p.Host == "" {
		p.Host = "localhost"
	}
	if len(parts) > 1 {
		p.Instance = parts[1]
	}
	if t.Prefix == "admin" {
		if p.Instance == "" {
			p.Port = 1434
		}
		p.BrowserMessage = BrowserDAC
	} else {
		p.BrowserMessage = BrowserAllInstances
	}
	return nil
}

func (t tcpParser) Protocol() string {
	return t.Prefix
}
