// Copyright 2023 Dolthub, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package sqlserver

import (
	"errors"
	"fmt"
	iofs "io/fs"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/google/uuid"

	"github.com/dolthub/dolt/go/libraries/doltcore/dbfactory"
	"github.com/dolthub/dolt/go/libraries/utils/filesys"
)

const ServerLocalCredsFile = "sql-server.info"

// LocalCreds is a struct that contains information about how to access the
// locally running server for a CLI process which wants to operate against a
// database while a sql-server process is running. It contains the pid of the
// process that created the lockfile, the port that the server is running on
// and the password to be used connecting to the server.
//
// Pid is a legacy field which is retained for compatibility but no longer
// influences the behavior of the running program(s).
type LocalCreds struct {
	Pid    int
	Port   int
	Secret string
}

func NewLocalCreds(port int) *LocalCreds {
	return &LocalCreds{os.Getpid(), port, uuid.New().String()}
}

// Best effort attempt to remove local creds file persisted as the Filesys
// rooted there.
func RemoveLocalCreds(fs filesys.Filesys) {
	credsFilePath, err := fs.Abs(filepath.Join(dbfactory.DoltDir, ServerLocalCredsFile))
	if err != nil {
		return
	}
	_ = fs.Delete(credsFilePath, false)
}

// WriteLocalCreds writes a file containing the contents of LocalCreds to the
// DoltDir rooted at the provided Filesys.
func WriteLocalCreds(fs filesys.Filesys, creds *LocalCreds) error {
	// if the DoltDir doesn't exist, create it.
	doltDir, err := fs.Abs(dbfactory.DoltDir)
	if err != nil {
		return err
	}
	err = fs.MkDirs(doltDir)
	if err != nil {
		return err
	}

	credsFile, err := fs.Abs(filepath.Join(dbfactory.DoltDir, ServerLocalCredsFile))
	if err != nil {
		return err
	}

	portStr := strconv.Itoa(creds.Port)
	if creds.Port < 0 {
		portStr = "-"
	}

	return fs.WriteFile(credsFile, []byte(fmt.Sprintf("%d:%s:%s", creds.Pid, portStr, creds.Secret)), 0600)
}

// Starting at `fs`, look for the a ServerLocalCredsFile in the .dolt directory
// of this directory and every parent directory, until we find one. When we
// find one, we return its contents if we can open and parse it successfully.
// Otherwise, we return an error associated with attempting to read it. If we
// do not find anything all the way up to the root of the filesystem, returns
// `nil` *LocalCreds and a `nil` error.
func FindAndLoadLocalCreds(fs filesys.Filesys) (creds *LocalCreds, err error) {
	root, err := fs.Abs(".")
	if err != nil {
		return nil, err
	}
	for root != "" && root[len(root)-1] != filepath.Separator {
		creds, err := LoadLocalCreds(fs)
		if err == nil {
			return creds, err
		}
		// If we have an error that is not ErrNotExist, for example, a
		// permission error opening the credentials file, or an error
		// indicating that the contents of the file were malformed, go
		// ahead and return the error and terminate our search here.
		if !errors.Is(err, iofs.ErrNotExist) {
			return nil, err
		}
		fs, err = fs.WithWorkingDir("..")
		if err != nil {
			return nil, err
		}
		root, err = fs.Abs(".")
		if err != nil {
			return nil, err
		}
	}
	return nil, nil
}

func LoadLocalCreds(fs filesys.Filesys) (creds *LocalCreds, err error) {
	rd, err := fs.OpenForRead(filepath.Join(dbfactory.DoltDir, ServerLocalCredsFile))
	if err != nil {
		return nil, err
	}
	defer rd.Close()

	b := make([]byte, 256)
	n, err := rd.Read(b)
	if err != nil {
		return nil, err
	}

	data := strings.TrimSpace(string(b[:n]))

	parts := strings.Split(data, ":")
	if len(parts) != 3 {
		return nil, errors.New("invalid lock file format")
	}

	pid, err := strconv.Atoi(parts[0])
	if err != nil {
		return nil, err
	}
	port := -1
	if parts[1] != "-" {
		port, err = strconv.Atoi(parts[1])
		if err != nil {
			return nil, err
		}
	}
	secret := parts[2]
	return &LocalCreds{Pid: pid, Port: port, Secret: secret}, nil
}
