package icecast

import (
	"context"
	"errors"
	"flag"
	"fmt"
	"log"
	"net/http"
	"net/url"
	"os"
	"os/exec"
	"sync"
	"time"

	"git.autistici.org/ale/autoradio"
	pb "git.autistici.org/ale/autoradio/proto"
	"git.autistici.org/ale/autoradio/util"
)

var (

	// Timeout for all HTTP requests to the local Icecast.
	icecastHTTPTimeout = 5 * time.Second

	icecastReloadCmd = flag.String("icecast-reload-command", "pkill -HUP icecast2", "Command to reload the icecast2 daemon")
)

// IcecastController manages a local Icecast daemon.
//
// Managing the local Icecast instance is a less simple task than it
// might look, as the details tend to be distribution-specific. We'd
// like to avoid implementing yet another service manager within
// radiod, so we are just going to assume that the Icecast daemon is
// managed using the local distribution-specific tools, and is running
// independently of radiod. Furthermore, we're going to make the
// following assumptions:
//
// - We have permissions to modify the Icecast config file;
//
// - We have installed our status XSLT page;
//
// - We can reload the Icecast service (send the icecast2 process a
// SIGHUP). Ideally we'd like to use the distribution-specific method,
// but with systemd we need to use "sudo". The code tries to
// autodetect a supported mechanism.
//
// The above is usually accomplished by running radiod as the same
// user that is running the Icecast daemon.
//
type Controller struct {
	configPath string
	adminPw    string
	port       int

	statusMx  sync.Mutex
	icecastOk bool
	mounts    []*pb.IcecastMount
}

// NewController returns a new Controller that manages a
// (independently started) local Icecast daemon. The context is used
// to cancel background processing.
func NewController(ctx context.Context, port int, configPath, adminPwPath string) (*Controller, error) {
	pw, err := getAdminPassword(adminPwPath)
	if err != nil {
		return nil, fmt.Errorf("couldn't initialize icecast admin password: %v", err)
	}

	ic := &Controller{
		configPath: configPath,
		adminPw:    pw,
		port:       port,
	}
	go ic.statusUpdater(ctx)
	return ic, nil
}

// Update reloads the Icecast daemon with a new configuration.
func (c *Controller) Update(ctx context.Context, mounts []*pb.Mount, isMaster bool, masterAddr string) error {
	if !isMaster && masterAddr == "" {
		return errors.New("unknown/invalid system state")
	}

	conf := newIcecastConfig(
		mounts,
		c.port,
		1000, // maxClients
		c.adminPw,
		isMaster,
		masterAddr,
	)
	data, err := conf.Encode()
	if err != nil {
		return err
	}

	changed, err := util.WriteFileIfChanged(c.configPath, data)
	if err != nil {
		return err
	}
	if changed {
		c.killSources(ctx, mounts)
		err = c.reload()
	}
	return err
}
func (c *Controller) reload() error {
	log.Printf("reloading icecast")
	cmd := exec.Command("/bin/sh", "-c", *icecastReloadCmd)
	cmd.Dir = "/"
	cmd.Stdout = os.Stderr
	cmd.Stderr = os.Stderr
	return cmd.Run()
}

var httpClient = &http.Client{}

func (c *Controller) killSources(ctx context.Context, mounts []*pb.Mount) {
	for _, m := range mounts {
		kctx, cancel := context.WithTimeout(ctx, icecastHTTPTimeout)
		if err := killSource(kctx, m, c.port, c.adminPw); err != nil {
			log.Printf("kill_sources: %s: error: %v", m.Path, err)
		}
		cancel()
	}
}

func killSource(ctx context.Context, m *pb.Mount, port int, pw string) error {
	v := make(url.Values)
	v.Set("mount", autoradio.MountPathToIcecastPath(m.Path))
	req, err := http.NewRequest("GET", fmt.Sprintf("http://localhost:%d/admin/killsource?%s", port, v.Encode()), nil)
	if err != nil {
		return err
	}
	req.SetBasicAuth("admin", pw)
	resp, err := httpClient.Do(req.WithContext(ctx))
	if err != nil {
		return err
	}
	resp.Body.Close()
	if resp.StatusCode != 200 {
		return fmt.Errorf("HTTP status %d", resp.StatusCode)
	}
	return nil
}