// Copyright © 2020 Intel Corporation
//
// SPDX-License-Identifier: GPL-3.0-only

package telemetry

import (
	"bytes"
	"crypto/md5"
	"crypto/rand"
	"fmt"
	"io/ioutil"
	"net"
	"net/url"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"
	"syscall"

	"github.com/clearlinux/clr-installer/cmd"
	"github.com/clearlinux/clr-installer/errors"
	"github.com/clearlinux/clr-installer/log"
	"github.com/clearlinux/clr-installer/network"
	"github.com/clearlinux/clr-installer/utils"
)

const (
	// RequiredBundle the bundle needed to use telemetry on the target
	RequiredBundle = "telemetrics"

	// Default Telemetry configuration file
	customTelemetryConf = "/etc/telemetrics/telemetrics.conf"
	telemetrySpoolDir   = "/var/spool/telemetry"

	// Title is a predefined text to display on the Telemetry
	Title = `Enable Telemetry`
	// Help is a predefined text to display on the Telemetry
	// screen for interactive installations
	Help = `Allow Clear Linux* OS to collect anonymized system data and usage
statistics for continuous improvement?  These reports only relate to
operating system details - no personally identifiable information
is collected.
`

	// TelemetryAboutURL is the URL to reference for telemetry details
	TelemetryAboutURL = `https://docs.01.org/clearlinux/guides/clear/telemetrics.html`

	// telemetry controlling interface command
	telemctlCmd = "telemctl"

	// RequestNotice is a common text string to be displayed when enabling
	// telemetry by default on local networks
	RequestNotice = `NOTICE: Enabling Telemetry preferred by default on internal networks.`

	// Default Telemetry server
	defaultTelemtryServer = "clr.telemetry.intel.com"

	maxPayload = 8 * 1024
	baseClass  = "org.clearlinux/clr-installer"

	// Detect hypervisor if running in VM
	envCmd = "/usr/bin/systemd-detect-virt"

	// recordGen command
	recordGenCmd = "telem-record-gen"

	// recordGenFullPath command
	recordGenCmdPath = "/usr/bin/" + recordGenCmd

	// Configuration template
	configTemplate = `[settings]
server=%s
tidheader=X-Telemetry-TID:\s%s
`
)

var (
	// Policy is the default Telemetry policy to be displayed
	// during interactive installations. Overridden by command line or
	// configuration options
	Policy = "Intel's privacy policy can be found at: http://www.intel.com/privacy."

	// ProgVersion is the version of this Clear Installer set from the Model
	// since telemetry is a component of the model, can directly include the model here
	ProgVersion string

	eventID string
)

// Telemetry represents the target system telemetry enabling flag
type Telemetry struct {
	Enabled     bool
	Defined     bool
	URL         string
	TID         string
	requested   bool
	server      string
	userDefined bool
}

// randomString generates hex string
func randomString() (string, error) {
	randData := make([]byte, 256)
	_, err := rand.Read(randData)
	if err != nil {
		return "", err
	}
	return fmt.Sprintf("%x", md5.Sum(randData)), nil
}

func init() {
	// Initialize the event record ID
	eventID, _ = randomString()
}

// SetUserDefined set the user defined flag
func (tl *Telemetry) SetUserDefined(userDefined bool) {
	tl.userDefined = userDefined
}

// IsUserDefined returns true if the configuration was interactively
// defined by the user
func (tl *Telemetry) IsUserDefined() bool {
	return tl.userDefined
}

// SetRequested set the Requested flag
func (tl *Telemetry) SetRequested(requested bool) {
	tl.requested = requested
}

// IsRequested returns true if we are requested telemetry be enabled
func (tl *Telemetry) IsRequested() bool {
	return tl.requested
}

// MarshalYAML marshals Telemetry into YAML format
func (tl *Telemetry) MarshalYAML() (interface{}, error) {
	if tl.userDefined {
		return tl.Enabled, nil
	}
	return nil, nil
}

// UnmarshalYAML unmarshals Telemetry from YAML format
func (tl *Telemetry) UnmarshalYAML(unmarshal func(interface{}) error) error {
	var enabled bool

	if err := unmarshal(&enabled); err != nil {
		return err
	}

	// Including telemetry in the YAML will set the default
	// as if the user had selected it in the UI
	tl.Enabled = enabled
	tl.userDefined = true
	return nil
}

// SetEnable sets the enabled flag and sets this is an user defined configuration
func (tl *Telemetry) SetEnable(enable bool) {
	tl.Enabled = enable

	if tl.server == "" {
		tl.server = defaultTelemtryServer
	}
}

// SetTelemetryServer set new defaults for the Telemetry server
// to override the built-in defaults
func (tl *Telemetry) SetTelemetryServer(telmURL string, telmID string, telmPolicy string) error {
	u, err := url.Parse(telmURL)
	if err != nil {
		return fmt.Errorf("Could not determine provided telemetry server name from URL (%s): %v", telmURL, err)
	}

	if urlErr := network.CheckURL(telmURL); urlErr != nil {
		return fmt.Errorf("Server not responding")
	}

	log.Debug("Using Telemetry URL: %q with TID: %q", telmURL, telmID)
	tl.URL = telmURL
	tl.TID = telmID
	tl.server = u.Hostname()

	// Set the policy
	Policy = telmPolicy

	return nil
}

// IsUsingPrivateIP return true if the current image is resolving
// the Telemetry server to a Private network IP address
func (tl *Telemetry) IsUsingPrivateIP() bool {
	inside := false

	if ips, err := net.LookupIP(tl.server); err == nil {
		// Create networks for all known Private Networks
		_, ipNetPriv10, _ := net.ParseCIDR("10.0.0.0/8")
		_, ipNetPriv172, _ := net.ParseCIDR("172.16.0.0/12")
		_, ipNetPriv192, _ := net.ParseCIDR("192.168.0.0/16")

		// Bounds checking to ensure we do not get stuck
		if len(ips) > 256 {
			ips = ips[:256]
		}

		for _, ip := range ips {
			if ip.DefaultMask() == nil {
				log.Warning("PrivateIP: Ignoring non-IPv4 IP address: %s", ip)
				continue
			}

			in := ipNetPriv10.Contains(ip) ||
				ipNetPriv172.Contains(ip) ||
				ipNetPriv192.Contains(ip)
			log.Debug("PrivateIP: Found IP: %s, Private IP?: %s", ip, strconv.FormatBool(in))
			if in {
				inside = true
			}
		}
	} else {
		log.Warning("PrivateIP: Could not determine network location: %v", err)
	}

	return inside
}

// CreateTelemetryConf create a custom Telemetry configuration file
// using the customer server and ID
func (tl *Telemetry) CreateTelemetryConf(rootDir string) error {
	// Ensure the customer configuration file directory exists
	targetConfFile := filepath.Join(rootDir, customTelemetryConf)
	targetConfDir := filepath.Dir(targetConfFile)
	if err := utils.MkdirAll(targetConfDir, 0755); err != nil {
		return err
	}

	var confFile = fmt.Sprintf(configTemplate, tl.URL, tl.TID)
	// Write the new file
	writeErr := ioutil.WriteFile(targetConfFile, []byte(confFile), 0644)
	if writeErr != nil {
		return writeErr
	}

	log.Debug("Created Telemetry server configuration file with URL %q and tag %q", tl.URL, tl.TID)

	return nil
}

// CopyTelemetryRecords copies the local spooled telemetry records
// to the target system to be uploaded when the target system is
// booted and telemetry is enabled.
func (tl *Telemetry) CopyTelemetryRecords(rootDir string) error {
	// Get directory ownership
	spoolDirInfo, err := os.Stat(telemetrySpoolDir)
	if err != nil {
		return fmt.Errorf("Unable to stat %q, %s", filepath.Join(telemetrySpoolDir), err)
	}
	sys := spoolDirInfo.Sys()
	stat, ok := sys.(*syscall.Stat_t)
	if !ok {
		return fmt.Errorf("Could not stat telemetry %q", telemetrySpoolDir)
	}

	err = filepath.Walk(telemetrySpoolDir,
		func(path string, info os.FileInfo, err error) error {
			if err != nil {
				log.Warning("Failure accessing a path %q: %v\n", path, err)
				return nil
			}
			target := filepath.Join(rootDir, path)
			if info.IsDir() {
				// Telemetry spool directory is flat
				return nil
			}
			if err := utils.CopyFile(path, target); err != nil {
				log.Warning("Failed to copy telemetry %q", target)
			}
			// Ensure all contents is owned correctly
			if err := os.Chown(target, int(stat.Uid), int(stat.Gid)); err != nil {
				log.Warning("Failed to change ownership of %q to UID:%d, GID:%d",
					target, stat.Uid, stat.Gid)
			}

			return nil
		})

	if err != nil {
		return fmt.Errorf("Failed to archive telemetry records")
	}

	return nil
}

// Installed returns true if telemetry tooling is present, false otherwise
func (tl *Telemetry) Installed(rootDir string) bool {
	bin := filepath.Join(rootDir, recordGenCmdPath)
	if _, err := os.Stat(bin); err != nil {
		if os.IsNotExist(err) {
			return false
		}
	}

	return true
}

// LogRecord generates and saves a Telemetry record
func (tl *Telemetry) LogRecord(class string, severity int, payload string) error {
	// Skip record generation if telemetry tooling is not present, and do not
	// return an error since telemetry bundle is not a requirement.
	if tl.Installed("") == false {
		log.Warning("Telemetry is not present in the installer, skip record generation")
		return nil
	}

	w := bytes.NewBuffer(nil)
	if severity < 1 {
		log.Warning("Telemetry severity (%d) less than 1, defaulting to 1", severity)
		severity = 1
	} else if severity > 4 {
		log.Warning("Telemetry severity (%d) greater than 4, defaulting to 4", severity)
		severity = 4
	}

	payload = "version=" + ProgVersion + "\n" + payload
	paySize := len(payload)
	if paySize > maxPayload {
		drop := payload[maxPayload:]
		payload = payload[0:(maxPayload - 1)]
		log.Warning("Telemetry payload greater than %d bytes, truncating: %q", maxPayload, drop)
	}

	args := []string{
		recordGenCmd,
		"--severity",
		fmt.Sprintf("%d", severity),
		"--class",
		fmt.Sprintf("%s/%s", baseClass, class),
		"--no-post",
		"--echo",
	}
	if eventID != "" {
		args = append(args,
			[]string{
				"--event-id", eventID,
			}...)
	}
	args = append(args,
		[]string{
			"--payload", payload,
		}...)

	err := cmd.Run(w, args...)
	if err != nil {
		return errors.Wrap(err)
	}

	recordName, err := randomString()
	if err != nil {
		return errors.Wrap(err)
	}
	recordName = recordName[:6]
	telemetryFilename := filepath.Join(telemetrySpoolDir, recordName)

	if err := ioutil.WriteFile(telemetryFilename, w.Bytes(), 0644); err != nil {
		log.Info(err.Error())
		return errors.Wrap(err)
	}

	return nil
}

// opt is a helper function
func opt(rootDir string, optCmd string) error {
	args := []string{
		"chroot",
		rootDir,
		telemctlCmd,
		optCmd,
	}

	err := cmd.RunAndLog(args...)
	if err != nil {
		return errors.Wrap(err)
	}

	return nil
}

// OptIn runs the command to opt-in to telemetry when users accept to
// provide telemetry from crashes
func (tl *Telemetry) OptIn(rootDir string) error {
	return opt(rootDir, "opt-in")
}

// OptOut runs the command to opt-out to telemetry when users do not
// agree to provide telemetry from crashes
func (tl *Telemetry) OptOut(rootDir string) error {
	return opt(rootDir, "opt-out")
}

// RunningEnvironment returns the name of the hypervisor if running in a
// virtual machine, otherwise none
func (tl *Telemetry) RunningEnvironment() string {
	out, err := exec.Command(envCmd).Output()
	if err == nil {
		return strings.TrimRight(string(out), "\n")
	}

	return "none"
}
