// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

//go:build linux

package tshttpproxy

import (
	"bytes"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"os"
	"strings"
	"sync"
	"time"

	"github.com/metacubex/tailscale/types/result"
	"github.com/metacubex/tailscale/util/lineiter"
)

// These vars are overridden for tests.
var (
	synologyProxyConfigPath = "/etc/proxy.conf"

	openSynologyProxyConf = func() (io.ReadCloser, error) {
		return os.Open(synologyProxyConfigPath)
	}
)

var cache struct {
	sync.Mutex
	httpProxy  *url.URL
	httpsProxy *url.URL
	updated    time.Time
}

func synologyProxyFromConfigCached(req *http.Request) (*url.URL, error) {
	if req.URL == nil {
		return nil, nil
	}

	cache.Lock()
	defer cache.Unlock()

	var err error
	modtime := mtime(synologyProxyConfigPath)

	if !modtime.Equal(cache.updated) {
		cache.httpProxy, cache.httpsProxy, err = synologyProxiesFromConfig()
		cache.updated = modtime
	}

	if req.URL.Scheme == "https" {
		return cache.httpsProxy, err
	}
	return cache.httpProxy, err
}

func synologyProxiesFromConfig() (*url.URL, *url.URL, error) {
	r, err := openSynologyProxyConf()
	if err != nil {
		if os.IsNotExist(err) {
			return nil, nil, nil
		}
		return nil, nil, err
	}
	defer r.Close()

	return parseSynologyConfig(r)
}

// parseSynologyConfig parses the Synology proxy configuration, and returns any
// http proxy, and any https proxy respectively, or an error if parsing fails.
func parseSynologyConfig(r io.Reader) (*url.URL, *url.URL, error) {
	cfg := map[string]string{}

	var parseErr error
	lineiter.Reader(r)(func(lr result.Of[[]byte]) bool {
		line, err := lr.Value()
		if err != nil {
			parseErr = err
			return false
		}
		// accept and skip over empty lines
		line = bytes.TrimSpace(line)
		if len(line) == 0 {
			return true
		}

		key, value, ok := strings.Cut(string(line), "=")
		if !ok {
			parseErr = fmt.Errorf("missing \"=\" in proxy.conf line: %q", line)
			return false
		}
		cfg[string(key)] = string(value)
		return true
	})
	if parseErr != nil {
		return nil, nil, parseErr
	}

	if cfg["proxy_enabled"] != "yes" {
		return nil, nil, nil
	}

	httpProxyURL := new(url.URL)
	httpsProxyURL := new(url.URL)
	if cfg["auth_enabled"] == "yes" {
		httpProxyURL.User = url.UserPassword(cfg["proxy_user"], cfg["proxy_pwd"])
		httpsProxyURL.User = url.UserPassword(cfg["proxy_user"], cfg["proxy_pwd"])
	}

	// As far as we are aware, synology does not support tls proxies.
	httpProxyURL.Scheme = "http"
	httpsProxyURL.Scheme = "http"

	httpsProxyURL = addHostPort(httpsProxyURL, cfg["https_host"], cfg["https_port"])
	httpProxyURL = addHostPort(httpProxyURL, cfg["http_host"], cfg["http_port"])

	return httpProxyURL, httpsProxyURL, nil
}

// addHostPort adds to u the given host and port and returns the updated url, or
// if host is empty, it returns nil.
func addHostPort(u *url.URL, host, port string) *url.URL {
	if host == "" {
		return nil
	}

	if port == "" {
		u.Host = host
	} else {
		u.Host = net.JoinHostPort(host, port)
	}
	return u
}

// mtime stat's path and returns its modification time. If path does not exist,
// it returns the unix epoch.
func mtime(path string) time.Time {
	fi, err := os.Stat(path)
	if err != nil {
		return time.Unix(0, 0)
	}
	return fi.ModTime()
}
