package tor

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"git.openprivacy.ca/openprivacy/bine/control"
	"git.openprivacy.ca/openprivacy/bine/process"
	"git.openprivacy.ca/openprivacy/bine/tor"
	bineed255192 "git.openprivacy.ca/openprivacy/bine/torutil/ed25519"
	"git.openprivacy.ca/openprivacy/connectivity"
	"git.openprivacy.ca/openprivacy/log"
	"golang.org/x/crypto/ed25519"
	"golang.org/x/crypto/sha3"
	"net"
	"net/textproto"
	"os"
	"os/exec"
	path "path/filepath"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"time"
)

const (
	minStatusIntervalMs = 200
	maxStatusIntervalMs = 2000
	restartCooldown     = time.Second * 30
)

const (
	networkUnknown = -3
	torDown        = -2
	networkDown    = -1
	networkUp      = 0
)

// NoTorrcError is a typed error thrown to indicate start could not complete due to lack of a torrc file
type NoTorrcError struct {
	path string
}

func (e *NoTorrcError) Error() string { return fmt.Sprintf("torrc file does not exist at %v", e.path) }

type logWriter struct {
	level log.Level
}

func (l *logWriter) Write(p []byte) (int, error) {
	log.Printf(l.level, "tor: %v", string(p))
	return len(p), nil
}

type onionListenService struct {
	lock sync.Mutex
	os   *tor.OnionService
	tp   *torProvider
}

type torProvider struct {
	controlPort     int
	t               *tor.Tor
	dialer          *tor.Dialer
	appDirectory    string
	bundeledTorPath string
	lock            sync.Mutex
	breakChan       chan bool
	childListeners  map[string]*onionListenService
	statusCallback  func(int, string)
	versionCallback func(string)
	lastRestartTime time.Time
	authenticator   tor.Authenticator
	isClosed        bool
	dataDir         string
	version         string
	bootProgress    int
}

func (ols *onionListenService) AddressFull() string {
	ols.lock.Lock()
	defer ols.lock.Unlock()
	return ols.os.Addr().String()
}

func (ols *onionListenService) Accept() (net.Conn, error) {
	return ols.os.Accept()
}

func (ols *onionListenService) Close() {
	ols.lock.Lock()
	defer ols.lock.Unlock()
	ols.os.Close()
}

func (tp *torProvider) GetInfo(onion string) (map[string]string, error) {
	tp.lock.Lock()
	defer tp.lock.Unlock()
	circuits, streams, err := getCircuitInfo(tp.t.Control)
	if err == nil {

		var circuitID string
		for _, stream := range streams {
			if stream.Key == "stream-status" {
				lines := strings.Split(stream.Val, "\n")
				for _, line := range lines {
					parts := strings.Split(line, " ")
					//  StreamID SP StreamStatus SP CircuitID SP Target CRLF
					if len(parts) == 4 {
						if strings.HasPrefix(parts[3], onion) {
							circuitID = parts[2]
							break
						}
					}
				}
			}
		}

		if circuitID == "" {
			return nil, errors.New("could not find circuit")
		}

		var hops []string
		for _, circuit := range circuits {
			if circuit.Key == "circuit-status" {
				lines := strings.Split(circuit.Val, "\n")
				for _, line := range lines {
					parts := strings.Split(line, " ")
					//  CIRCID SP STATUS SP PATH...

					if len(parts) >= 3 && circuitID == parts[0] {
						//log.Debugf("Found Circuit for Onion %v %v", onion, parts)
						if parts[1] == "BUILT" {
							circuitPath := strings.Split(parts[2], ",")
							for _, hop := range circuitPath {
								fingerprint := hop[1:41]
								keyvals, err := tp.t.Control.GetInfo(fmt.Sprintf("ns/id/%s", fingerprint))
								if err == nil && len(keyvals) == 1 {
									lines = strings.Split(keyvals[0].Val, "\n")
									for _, line := range lines {
										if strings.HasPrefix(line, "r") {
											parts := strings.Split(line, " ")
											if len(parts) > 6 {
												keyvals, err := tp.t.Control.GetInfo(fmt.Sprintf("ip-to-country/%s", parts[6]))
												if err == nil && len(keyvals) >= 1 {
													hops = append(hops, fmt.Sprintf("%s:%s", strings.ToUpper(keyvals[0].Val), parts[6]))
												} else {
													hops = append(hops, fmt.Sprintf("%s:%s", "XX", parts[6]))
												}
											}
										}
									}
								}
							}

							return map[string]string{"circuit": strings.Join(hops, ",")}, nil

						}
					}
				}
			}
		}
	}
	return nil, err
}

var progRe = regexp.MustCompile("PROGRESS=([0-9]*)")
var sumRe = regexp.MustCompile("SUMMARY=\"(.*)\"$")

// GetBootstrapStatus returns an int 0-100 on the percent the bootstrapping of the underlying network is at and an optional string message
//
//	returns -1 on network disconnected
//	returns -2 on error
func (tp *torProvider) GetBootstrapStatus() (int, string) {
	tp.lock.Lock()
	defer tp.lock.Unlock()

	if tp.t == nil {
		return torDown, "error: no tor, trying to restart..."
	}

	val, err := tp.t.Control.GetInfo("network-liveness")
	if err != nil {
		return torDown, "can't query tor network-liveness"
	}
	if val[0].Val == "down" {
		return networkDown, "tor cannot detect underlying network"
	}
	// else network is up

	kvs, err := tp.t.Control.GetInfo("status/bootstrap-phase")
	if err != nil {
		return torDown, "error querrying status/bootstrap-phase"
	}
	progress := 0
	status := ""

	if len(kvs) > 0 {

		if progMatches := progRe.FindStringSubmatch(kvs[0].Val); len(progMatches) > 1 {
			progress, _ = strconv.Atoi(progMatches[1])
		}

		if statusMatches := sumRe.FindStringSubmatch(kvs[0].Val); len(statusMatches) > 1 {
			status = statusMatches[1]
		}
	}
	tp.bootProgress = progress
	return progress, status
}

func (tp *torProvider) GetVersion() string {
	tp.lock.Lock()
	defer tp.lock.Unlock()
	return tp.version
}

func (tp *torProvider) closed() bool {
	tp.lock.Lock()
	defer tp.lock.Unlock()
	return tp.isClosed
}

// WaitTillBootstrapped Blocks until underlying network is bootstrapped
func (tp *torProvider) WaitTillBootstrapped() error {
	for !tp.closed() {
		progress, _ := tp.GetBootstrapStatus()
		if progress == 100 {
			return nil
		}
		time.Sleep(100 * time.Millisecond)
	}
	return errors.New("close called before bootstrap")
}

func (tp *torProvider) Listen(identity connectivity.PrivateKey, port int) (connectivity.ListenService, error) {
	tp.lock.Lock()
	defer tp.lock.Unlock()

	if tp.t == nil {
		return nil, errors.New("tor provider closed")
	}

	var onion string
	var privkey ed25519.PrivateKey

	switch pk := identity.(type) {
	case ed25519.PrivateKey:
		privkey = pk
		gpubk := pk.Public()
		switch pubk := gpubk.(type) {
		case ed25519.PublicKey:
			onion = GetTorV3Hostname(pubk)
		default:
			return nil, fmt.Errorf("unknown public key type %v", pubk)
		}
	default:
		return nil, fmt.Errorf("unknown private key type %v", pk)
	}

	// Hack around tor detached onions not having a more obvious resume mechanism
	// So we use deterministic ports
	seedbytes := sha3.New224().Sum([]byte(onion))
	localport := int(seedbytes[0]) + (int(seedbytes[1]) << 8)
	if localport < 1024 { // this is not uniformly random, but we don't need it to be
		localport += 1024
	}

	var localListener net.Listener
	var err error

	if cwtchRestrictPorts := os.Getenv("CWTCH_RESTRICT_PORTS"); strings.ToLower(cwtchRestrictPorts) == "true" {
		// for whonix like systems we tightly restrict possible listen...
		// pick a random port between 15000 and 15378
		// cwtch = 63 *77 *74* 63* 68 = 1537844616
		log.Infof("using restricted ports, CWTCH_RESTRICT_PORTS=true")
		localport = 15000 + (localport % 378)
	}

	if bindExternal := os.Getenv("CWTCH_BIND_EXTERNAL_WHONIX"); strings.ToLower(bindExternal) == "true" {
		if _, ferr := os.Stat("/usr/share/anon-ws-base-files/workstation"); !os.IsNotExist(ferr) {
			log.Infof("WARNING: binding to external interfaces. This is potentially unsafe outside of a containerized environment.")
			localListener, err = net.Listen("tcp", "0.0.0.0:"+strconv.Itoa(localport))
		} else {
			log.Errorf("CWTCH_BIND_EXTERNAL_WHONIX flag set, but /usr/share/anon-ws-base-files/workstation does not exist. Defaulting to binding to local ports")
			localListener, err = net.Listen("tcp", "127.0.0.1:"+strconv.Itoa(localport))
		}
	} else {
		localListener, err = net.Listen("tcp", "127.0.0.1:"+strconv.Itoa(localport))
	}

	if err != nil {
		return nil, err
	}

	conf := &tor.ListenConf{NoWait: true, Version3: true, Key: identity, RemotePorts: []int{port}, Detach: true, DiscardKey: true, LocalListener: localListener}
	os, err := tp.t.Listen(context.TODO(), conf)

	// Reattach to the old local listener...
	// Note: this code probably shouldn't be hit in Cwtch anymore because we purge torrc on restart.
	if err != nil && strings.Contains(err.Error(), "550 Unspecified Tor error: Onion address collision") {
		log.Errorf("550 Unspecified Tor error: Onion address collision - Recovering, but this probably indicates some weird tor configuration issue...")
		os = &tor.OnionService{Tor: tp.t, LocalListener: localListener, ID: onion, Version3: true, Key: bineed255192.FromCryptoPrivateKey(privkey), ClientAuths: make(map[string]string), RemotePorts: []int{port}}
		err = nil
	}

	// Any other errors require an immediate return as os is likely nil...
	if err != nil {
		return nil, err
	}

	// We need to set os.ID here, otherwise os.Close() may not shut down the onion service properly...
	os.ID = onion
	os.CloseLocalListenerOnClose = true

	ols := &onionListenService{os: os, tp: tp}
	tp.childListeners[ols.AddressFull()] = ols
	return ols, nil
}

func (tp *torProvider) Restart() {
	log.Debugf("launching restart...")

	tp.lock.Lock()
	log.Debugf("checking last restart time")
	if time.Since(tp.lastRestartTime) < restartCooldown {
		tp.lock.Unlock()
		return
	}
	tp.lock.Unlock()
	go tp.restart()
}

func (tp *torProvider) restart() {
	log.Debugf("Waiting for Tor to Close...")
	tp.callStatusCallback(0, "rebooting")
	tp.Close()
	tp.lock.Lock()
	defer tp.lock.Unlock()

	// preserve status callback after shutdown
	statusCallback := tp.statusCallback
	versionCallback := tp.versionCallback

	tp.t = nil
	log.Debugf("Restarting Tor Process")
	newTp, err := startTor(tp.appDirectory, tp.bundeledTorPath, tp.dataDir, tp.controlPort, tp.authenticator)
	if err == nil {
		// we need to reassign tor, dialer and callback which will have changed by swapping out
		// the underlying connection.
		tp.t = newTp.t
		tp.dialer = newTp.dialer
		tp.statusCallback = statusCallback
		tp.versionCallback = versionCallback
		if tp.versionCallback != nil {
			tp.versionCallback(tp.version)
		}
		tp.lastRestartTime = time.Now()
		tp.isClosed = false
		go tp.monitorRestart()
	} else {
		log.Errorf("Error restarting Tor process: %v", err)
	}
}

func (tp *torProvider) Open(hostname string) (net.Conn, string, error) {
	tp.lock.Lock()

	if tp.t == nil {
		tp.lock.Unlock()
		return nil, hostname, errors.New("tor is offline")
	}
	if tp.bootProgress != 100 {
		tp.lock.Unlock()
		return nil, hostname, fmt.Errorf("tor not online, bootstrap progress only %v", tp.bootProgress)
	}
	tp.lock.Unlock()

	resolvedHostname := hostname
	if strings.HasPrefix(hostname, "ricochet:") {
		addrParts := strings.Split(hostname, ":")
		resolvedHostname = addrParts[1]
	}
	conn, err := tp.dialer.Dial("tcp", resolvedHostname+".onion:9878")
	return conn, resolvedHostname, err
}

func (tp *torProvider) Close() {
	closing := false
	tp.lock.Lock()
	defer tp.lock.Unlock()
	if !tp.isClosed {
		// Break out of any background checks and close
		// the underlying tor connection
		tp.isClosed = true
		tp.breakChan <- true
		// wiggle lock to make sure if monitorRestart is waiting for the lock, it gets it, and finished that branch and gets the channel request to exit
		tp.lock.Unlock()
		tp.lock.Lock()
		closing = true
	}

	// Unregister Child Listeners
	for addr, child := range tp.childListeners {
		child.Close()
		delete(tp.childListeners, addr)
	}

	log.Debugf("shutting down acn threads..(is already closed: %v)", tp.isClosed)

	if closing {
		if tp.t != nil {
			tp.t.Close()
			tp.t = nil
		}
	}
}

func (tp *torProvider) SetStatusCallback(callback func(int, string)) {
	tp.lock.Lock()
	defer tp.lock.Unlock()
	tp.statusCallback = callback
}

func (tp *torProvider) SetVersionCallback(callback func(string)) {
	tp.lock.Lock()
	defer tp.lock.Unlock()
	tp.versionCallback = callback
}

func (tp *torProvider) GetStatusCallback() func(int, string) {
	tp.lock.Lock()
	defer tp.lock.Unlock()
	return tp.statusCallback
}

func (tp *torProvider) GetVersionCallback() func(string) {
	tp.lock.Lock()
	defer tp.lock.Unlock()
	return tp.versionCallback
}

func (tp *torProvider) callStatusCallback(prog int, status string) {
	tp.lock.Lock()
	defer tp.lock.Unlock()
	if tp.statusCallback != nil {
		tp.statusCallback(prog, status)
	}
}

// NewTorACNWithAuth creates/starts a Tor ACN and returns a usable ACN object
func NewTorACNWithAuth(appDirectory string, bundledTorPath string, dataDir string, controlPort int, authenticator tor.Authenticator) (connectivity.ACN, error) {
	tp, err := startTor(appDirectory, bundledTorPath, dataDir, controlPort, authenticator)
	if err == nil {
		tp.isClosed = false
		go tp.monitorRestart()
	}
	return tp, err
}

// newHideCmd creates a Creator function for bine which generates a cmd that one windows will hide the dosbox
func newHideCmd(exePath string) process.Creator {
	return process.CmdCreatorFunc(func(ctx context.Context, args ...string) (*exec.Cmd, error) {
		loggerDebug := &logWriter{log.LevelDebug}
		loggerError := &logWriter{log.LevelError}

		cmd := exec.CommandContext(ctx, exePath, args...)
		cmd.Stdout = loggerDebug
		cmd.Stderr = loggerError
		cmd.SysProcAttr = sysProcAttr

		// override tor ld_library_path if requested
		torLdLibPathEnv, exists := os.LookupEnv("TOR_LD_LIBRARY_PATH")
		if exists {
			ldLibPath := fmt.Sprintf("LD_LIBRARY_PATH=%v", torLdLibPathEnv)
			cmd.Env = append([]string{ldLibPath}, os.Environ()...)
		} else {
			// otherwise set it to include the directory the tor binary is in
			// as our bundled tor comes with its own libevent, libssl, libcrypto and etc
			torLdLibPath := path.Dir(exePath)
			if _, err := os.Stat(torLdLibPath); err == nil {
				ldLibPath := fmt.Sprintf("LD_LIBRARY_PATH=%v", torLdLibPath)
				cmd.Env = append([]string{ldLibPath}, os.Environ()...)
			}
		}

		return cmd, nil
	})
}

func (tp *torProvider) checkVersion() (string, error) {
	// attempt connect to system tor
	log.Debugf("dialing tor control port")
	controlport, err := dialControlPort(tp.controlPort)
	if err == nil {
		defer controlport.Close()
		err := tp.authenticator.Authenticate(controlport)
		if err == nil {
			log.Debugln("connected to control port")
			pinfo, err := controlport.ProtocolInfo()
			if err == nil {
				if minTorVersionReqs(pinfo.TorVersion) {
					log.Debugln("OK version " + pinfo.TorVersion)
					return pinfo.TorVersion, nil
				}
				return pinfo.TorVersion, fmt.Errorf("tor version not supported: %v", pinfo.TorVersion)
			}
		}
	}
	return "", err
}

func startTor(appDirectory string, bundledTorPath string, dataDir string, controlPort int, authenticator tor.Authenticator) (*torProvider, error) {
	torDir := path.Join(appDirectory, "tor")

	os.MkdirAll(torDir, 0700)

	tp := &torProvider{authenticator: authenticator, controlPort: controlPort, appDirectory: appDirectory, bundeledTorPath: bundledTorPath, childListeners: make(map[string]*onionListenService), breakChan: make(chan bool, 1), statusCallback: nil, versionCallback: nil, lastRestartTime: time.Now().Add(-restartCooldown), bootProgress: -1}

	log.Debugf("checking if there is a running system tor")
	if version, err := tp.checkVersion(); err == nil {
		tp.version = version
		controlport, err := dialControlPort(tp.controlPort)
		if err == nil {
			log.Debugf("creating tor handler from system tor")
			tp.t = createFromExisting(controlport, dataDir)
			tp.dialer, err = tp.t.Dialer(context.TODO(), &tor.DialConf{Authenticator: tp.authenticator})
			return tp, err
		}
	}

	// check if the torrc file is present where expected
	if _, err := os.Stat(path.Join(torDir, "torrc")); os.IsNotExist(err) {
		err = &NoTorrcError{path.Join(torDir, "torrc")}
		log.Debugln(err.Error())
		return nil, err
	}

	// if not, try bundled tor, then running system tor
	log.Debugln("checking if we can run bundled tor or system installed tor")
	if version, pass := checkCmdlineTorVersion(bundledTorPath); pass {
		log.Debugln("bundled tor appears viable, attempting to use '" + bundledTorPath + "'")
		t, err := tor.Start(context.TODO(), &tor.StartConf{ControlPort: tp.controlPort, NoAutoSocksPort: true, DisableCookieAuth: true, UseEmbeddedControlConn: false, DisableEagerAuth: true, EnableNetwork: true, DataDir: dataDir, TorrcFile: path.Join(torDir, "torrc"), ExePath: bundledTorPath, DebugWriter: nil, ProcessCreator: newHideCmd(bundledTorPath)})
		if err != nil {
			log.Debugf("Error running bundled tor %v\n", err)
			return nil, err
		}
		tp.t = t
		tp.version = version
	} else if version, pass := checkCmdlineTorVersion("tor"); pass {
		t, err := tor.Start(context.TODO(), &tor.StartConf{ControlPort: tp.controlPort, NoAutoSocksPort: true, DisableCookieAuth: true, UseEmbeddedControlConn: false, DisableEagerAuth: true, EnableNetwork: true, DataDir: dataDir, TorrcFile: path.Join(torDir, "torrc"), DebugWriter: nil, ProcessCreator: newHideCmd("tor")})
		if err != nil {
			log.Debugf("Error connecting to self-run system tor: %v\n", err)
			return nil, err
		}
		tp.t = t
		tp.version = version
	} else {
		log.Debugln("Could not find a viable tor running or to run")
		return nil, fmt.Errorf("could not connect to or start Tor that met requirements (min Tor version 0.3.5.x)")
	}

	version, err := tp.checkVersion()
	if err == nil {
		tp.t.DeleteDataDirOnClose = false // caller is responsible for dealing with cached information...
		tp.dialer, err = tp.t.Dialer(context.TODO(), &tor.DialConf{Authenticator: tp.authenticator})
		tp.version = version
		tp.t.Control.TakeOwnership()
		return tp, err
	}
	return nil, fmt.Errorf("could not connect to running tor: %v", err)
}

func (tp *torProvider) GetPID() (int, error) {
	val, err := tp.t.Control.GetInfo("process/pid")
	if err == nil {
		return strconv.Atoi(val[0].Val)
	}
	return 0, err
}

func (tp *torProvider) monitorRestart() {
	lastBootstrapProgress := networkUnknown
	interval := minStatusIntervalMs

	for {
		select {
		case <-time.After(time.Millisecond * time.Duration(interval)):
			if interval < maxStatusIntervalMs {
				interval *= 2
			}

			prog, status := tp.GetBootstrapStatus()

			if prog == torDown && tp.t != nil {
				tp.restart()
				return
			}

			if prog != lastBootstrapProgress {
				tp.callStatusCallback(prog, status)
				interval = minStatusIntervalMs
				lastBootstrapProgress = prog
			}

		case <-tp.breakChan:
			return
		}
	}
}

func getCircuitInfo(controlport *control.Conn) ([]*control.KeyVal, []*control.KeyVal, error) {
	circuits, cerr := controlport.GetInfo("circuit-status")
	streams, serr := controlport.GetInfo("stream-status")
	if cerr == nil && serr == nil {
		return circuits, streams, nil
	}
	return nil, nil, errors.New("could not fetch circuits or streams")
}

func createFromExisting(controlport *control.Conn, datadir string) *tor.Tor {
	t := &tor.Tor{
		Process:              nil,
		Control:              controlport,
		ProcessCancelFunc:    nil,
		DataDir:              datadir,
		DeleteDataDirOnClose: false,
		DebugWriter:          nil,
		StopProcessOnClose:   false,
		GeoIPCreatedFile:     "",
		GeoIPv6CreatedFile:   "",
	}
	t.Control.DebugWriter = t.DebugWriter

	return t
}

func checkCmdlineTorVersion(torCmd string) (string, bool) {
	if torCmd == "" {
		return "", false
	}
	// ideally we would use CommandContext with Timeout here
	// but it doesn't work with programs that may launch other processes via scripts e.g. exec
	// and the workout is more complex than just implementing the logic ourselves...
	cmd := exec.Command(torCmd, "--version")
	var outb bytes.Buffer
	cmd.Stdout = &outb

	waiting := make(chan error, 1)

	// try running the tor process
	go func() {
		log.Debugf("running tor process: %v", torCmd)
		cmd.Run()
		waiting <- nil
	}()

	// timeout function
	go func() {
		<-time.After(time.Second * 5)
		waiting <- errors.New("timeout")
	}()

	err := <-waiting

	if err != nil {
		log.Debugf("tor process timed out")
		return "", false
	}

	re := regexp.MustCompile(`[0-1]+\.[0-9]+\.[0-9]+\.[0-9]+`)
	sysTorVersion := re.Find(outb.Bytes())
	log.Infof("tor version: %v", string(sysTorVersion))
	return string(sysTorVersion), minTorVersionReqs(string(sysTorVersion))
}

// returns true if supplied version meets our min requirments
// min requirement: 0.3.5.x
func minTorVersionReqs(torversion string) bool {
	torversions := strings.Split(torversion, ".") //eg: 0.3.4.8 or 0.3.5.1-alpha
	if len(torversions) >= 3 {
		log.Debugf("torversions: %v", torversions)
		tva, _ := strconv.Atoi(torversions[0])
		tvb, _ := strconv.Atoi(torversions[1])
		tvc, _ := strconv.Atoi(torversions[2])
		return tva > 0 || (tva == 0 && (tvb > 3 || (tvb == 3 && tvc >= 5)))
	}
	return false
}

func dialControlPort(port int) (*control.Conn, error) {
	textConn, err := textproto.Dial("tcp", "127.0.0.1:"+strconv.Itoa(port))
	if err != nil {
		return nil, err
	}
	return control.NewConn(textConn), nil
}
