package taskfile

import (
	"context"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"slices"
	"strings"
	"time"

	"github.com/go-task/task/v3/errors"
	"github.com/go-task/task/v3/internal/filepathext"
	"github.com/go-task/task/v3/internal/logger"
	"github.com/go-task/task/v3/internal/sysinfo"
)

var (
	defaultTaskfiles = []string{
		"Taskfile.yml",
		"taskfile.yml",
		"Taskfile.yaml",
		"taskfile.yaml",
		"Taskfile.dist.yml",
		"taskfile.dist.yml",
		"Taskfile.dist.yaml",
		"taskfile.dist.yaml",
	}
	allowedContentTypes = []string{
		"text/plain",
		"text/yaml",
		"text/x-yaml",
		"application/yaml",
		"application/x-yaml",
	}
)

// RemoteExists will check if a file at the given URL Exists. If it does, it
// will return its URL. If it does not, it will search the search for any files
// at the given URL with any of the default Taskfile files names. If any of
// these match a file, the first matching path will be returned. If no files are
// found, an error will be returned.
func RemoteExists(ctx context.Context, l *logger.Logger, u *url.URL, timeout time.Duration) (*url.URL, error) {
	// Create a new HEAD request for the given URL to check if the resource exists
	req, err := http.NewRequest("HEAD", u.String(), nil)
	if err != nil {
		return nil, errors.TaskfileFetchFailedError{URI: u.String()}
	}

	// Request the given URL
	resp, err := http.DefaultClient.Do(req.WithContext(ctx))
	if err != nil {
		if errors.Is(ctx.Err(), context.DeadlineExceeded) {
			return nil, &errors.TaskfileNetworkTimeoutError{URI: u.String(), Timeout: timeout}
		}
		return nil, errors.TaskfileFetchFailedError{URI: u.String()}
	}
	defer resp.Body.Close()

	// If the request was successful and the content type is allowed, return the
	// URL The content type check is to avoid downloading files that are not
	// Taskfiles It means we can try other files instead of downloading
	// something that is definitely not a Taskfile
	contentType := resp.Header.Get("Content-Type")
	if resp.StatusCode == http.StatusOK && slices.ContainsFunc(allowedContentTypes, func(s string) bool {
		return strings.Contains(contentType, s)
	}) {
		return u, nil
	}

	// If the request was not successful, append the default Taskfile names to
	// the URL and return the URL of the first successful request
	for _, taskfile := range defaultTaskfiles {
		// Fixes a bug with JoinPath where a leading slash is not added to the
		// path if it is empty
		if u.Path == "" {
			u.Path = "/"
		}
		alt := u.JoinPath(taskfile)
		req.URL = alt

		// Try the alternative URL
		resp, err = http.DefaultClient.Do(req)
		if err != nil {
			return nil, errors.TaskfileFetchFailedError{URI: u.String()}
		}
		defer resp.Body.Close()

		// If the request was successful, return the URL
		if resp.StatusCode == http.StatusOK {
			l.VerboseOutf(logger.Magenta, "task: [%s] Not found - Using alternative (%s)\n", alt.String(), taskfile)
			return alt, nil
		}
	}

	return nil, errors.TaskfileNotFoundError{URI: u.String(), Walk: false}
}

// Exists will check if a file at the given path Exists. If it does, it will
// return the path to it. If it does not, it will search for any files at the
// given path with any of the default Taskfile files names. If any of these
// match a file, the first matching path will be returned. If no files are
// found, an error will be returned.
func Exists(l *logger.Logger, path string) (string, error) {
	fi, err := os.Stat(path)
	if err != nil {
		return "", err
	}
	if fi.Mode().IsRegular() ||
		fi.Mode()&os.ModeDevice != 0 ||
		fi.Mode()&os.ModeSymlink != 0 ||
		fi.Mode()&os.ModeNamedPipe != 0 {
		return filepath.Abs(path)
	}

	for _, taskfile := range defaultTaskfiles {
		alt := filepathext.SmartJoin(path, taskfile)
		if _, err := os.Stat(alt); err == nil {
			l.VerboseOutf(logger.Magenta, "task: [%s] Not found - Using alternative (%s)\n", path, taskfile)
			return filepath.Abs(alt)
		}
	}

	return "", errors.TaskfileNotFoundError{URI: path, Walk: false}
}

// ExistsWalk will check if a file at the given path exists by calling the
// exists function. If a file is not found, it will walk up the directory tree
// calling the exists function until it finds a file or reaches the root
// directory. On supported operating systems, it will also check if the user ID
// of the directory changes and abort if it does.
func ExistsWalk(l *logger.Logger, path string) (string, error) {
	origPath := path
	owner, err := sysinfo.Owner(path)
	if err != nil {
		return "", err
	}
	for {
		fpath, err := Exists(l, path)
		if err == nil {
			return fpath, nil
		}

		// Get the parent path/user id
		parentPath := filepath.Dir(path)
		parentOwner, err := sysinfo.Owner(parentPath)
		if err != nil {
			return "", err
		}

		// Error if we reached the root directory and still haven't found a file
		// OR if the user id of the directory changes
		if path == parentPath || (parentOwner != owner) {
			return "", errors.TaskfileNotFoundError{URI: origPath, Walk: false}
		}

		owner = parentOwner
		path = parentPath
	}
}