// Package rcon provices a password-protected remote web interface for managing
// a Hnakra router. For a user to be able to access the control panel, the user
// entry in the configuration file must have the special string (rcon) listed as
// an allowed pattern.
package rcon

import "io"
import "fmt"
import "net/url"
import "net/http"
import "hnakra/config"
import "hnakra/router"
import "hnakra/rotate"
import "hnakra/protocol"

const css = `
body { background-color: #161815; color: #aea894; margin: 3em; }
h1 { font-size: 1.5em; }
h2 { font-size: 1.2em; }
a { color: #69905f; }
table { border-collapse: collapse; }
th, td {
	border: 1px solid #272d24;
	padding: 0.5em;
	text-align: left;
	vertical-align: top;
}`

type Rcon struct {
	root       string
	config     config.Config
	router     *router.Router
	mux        *http.ServeMux
	lines      []string
	lineWriter io.Writer
}

func New (root string) *Rcon {
	if len(root) > 0 && root[len(root) - 1] != '/' {
		root += "/"
	}
	rcon := &Rcon {
		root: root,
		mux:  http.NewServeMux(),
	}
	rcon.lineWriter = rotate.LineWriter(rcon.pushLine)
	rcon.mux.HandleFunc(root + "", rcon.serveMainMenu)
	rcon.mux.HandleFunc(root + "log", rcon.serveLog)
	rcon.mux.HandleFunc(root + "services", rcon.serveServices)
	rcon.mux.HandleFunc(root + "config", rcon.serveConfig)
	rcon.mux.HandleFunc(root + "details", rcon.serveServiceDetails)
	return rcon
}

func (rcon *Rcon) SetConfig (config config.Config) {
	rcon.config = config
}

func (rcon *Rcon) SetRouter (router *router.Router) {
	rcon.router = router
}

func (rcon *Rcon) pushLine (buffer []byte) (n int, err error) {
	const LEN_CAP = 128

	n = len(buffer)
	line := string(buffer)
	rcon.lines = append(rcon.lines, line)
	if len(rcon.lines) > LEN_CAP {
		rcon.lines = rcon.lines[len(rcon.lines) - LEN_CAP:]
	}

	return
}

func (rcon *Rcon) Write (buffer []byte) (int, error) {
	return rcon.lineWriter.Write(buffer)
}

func (rcon *Rcon) ServeHTTP (res http.ResponseWriter, req *http.Request) {
	// disallow accessing rcon over an insecure connection
	if req.TLS == nil {
		res.Header().Add("content-type", "text/html")
		res.WriteHeader(http.StatusTeapot)
		res.Write([]byte (
		`<!DOCTYPE html>
		<html>
		<head>
			<title>HTTP Not Allowed</title>
			<style>` + css + `</style>
		</head>
		<body>
			<h1>HTTP Not Allowed</h1>
			<p>RCON is not available over HTTP for security reasons.
			Please try again with HTTPS.</p>
		</body>
		</html>`))
		return
	}

	fail := func () {
		res.Header().Add("WWW-Authenticate", "Basic realm=rcon")
		res.WriteHeader(http.StatusUnauthorized)
	}

	// need authentication for accessing rcon menu
	name, key, ok := req.BasicAuth()
	if ok {
		user := rcon.config.User(name)
		if !user.Validate([]byte(key)) { fail(); return }
	} else {
		fail(); return
	}

	// if the user is authorized, serve page as normal
	rcon.mux.ServeHTTP(res, req)
}

func (rcon *Rcon) serveMainMenu (res http.ResponseWriter, req *http.Request) {
	if req.URL.Path != rcon.root {
		http.NotFoundHandler().ServeHTTP(res, req)
		return
	}

	res.Header().Add("content-type", "text/html")
	res.WriteHeader(http.StatusFound)
	res.Write([]byte (
	`<!DOCTYPE html>
	<html>
	<head>
		<title>RCON Menu</title>
		<style>` + css + `</style>
	</head>
	<body>
		<h1>RCON Menu</h1>
		<ul>
			<li><a href=log>log</a></li>
			<li><a href=services>services</a></li>
			<li><a href=config>config</a></li>
		</ul>
	</body>
	</html>`))
}

func (rcon *Rcon) serveLog (res http.ResponseWriter, req *http.Request) {
	res.Header().Add("content-type", "text/html")
	res.WriteHeader(http.StatusFound)
	res.Write([]byte (
	`<!DOCTYPE html>
	<html>
	<head>
		<title>Router Log</title>
		<style>` + css + `</style>
	</head>
	<body>
		<h1>Router Log</h1><pre>`))

	for _, line := range rcon.lines {
		res.Write([]byte(line))
	}
	
	res.Write([]byte (
	`</pre></body>
	</html>`))
}

func (rcon *Rcon) serveServices (res http.ResponseWriter, req *http.Request) {
	if rcon.router == nil {
		rcon.serveRouterOffline(res, req)
		return
	}
	
	res.Header().Add("content-type", "text/html")
	res.WriteHeader(http.StatusFound)
	res.Write([]byte (
	`<!DOCTYPE html>
	<html>
	<head>
		<title>Connected Services</title>
		<style>` + css + `</style>
	</head>
	<body>
		<h1>Connected Services</h1>
		<table>
		<thead>
			<tr>
			<th>user</th>
			<th>name</th>
			<th>description</th>
			<th>pattern</th>
			</tr>
		</thead>
		<tbody>
	`))

	rcon.router.OverServices(func (service *router.Service) bool {
		res.Write([]byte(fmt.Sprintf (
			`<tr><td>%s</td><td><a href="details?service=%s">%s</a></td><td>%s</td><td>%s</td></tr>`,
			service.User(),
			service.Name(),
			service.Name(),
			service.Description(),
			service.Pattern())))
		return true
	})
	
	res.Write([]byte (
	`</tbody></table></body>
	</html>`))
}

func (rcon *Rcon) serveConfig (res http.ResponseWriter, req *http.Request) {
	res.Header().Add("content-type", "text/html")
	res.WriteHeader(http.StatusFound)
	res.Write([]byte (
	`<!DOCTYPE html>
	<html>
	<head>
		<title>Router Config</title>
		<style>` + css + `</style>
	</head>
	<body>
		<h1>Router Config</h1>
		<h2>Users</h2>
		<table>
		<thead>
			<tr>
			<th>name</th>
			<th>rcon</th>
			<th>patterns</th>
			</tr>
		</thead>
		<tbody>`))

	rcon.config.OverUsers(func (name string, user config.User) bool {
		patterns := ""
		user.OverPatterns (func (pattern string) bool {
			if patterns != "" {
				patterns += " "
			}
			patterns += pattern
			return true
		})
		res.Write([]byte(fmt.Sprintf (
			"<tr><td>%s</td><td>%v</td><td>%s</td></tr>",
			name, user.RconAllow(), patterns)))
		return true
	})

	res.Write([]byte(`</tbody></table><h2>Aliases</h2>`))
	fallback := rcon.config.AliasFallback()
	if fallback != "" {
		res.Write([]byte(`<p>Fallback: ` + fallback + `</p>`))
	}
	res.Write([]byte(`
		<table>
		<thead>
			<tr>
			<th>alias</th>
			<th>target</th>
			</tr>
		</thead>
		<tbody>`))
	rcon.config.OverAliases(func (alias, target string) bool {
		res.Write([]byte(fmt.Sprintf (
			"<tr><td>%s</td><td>%s</td></tr>",
			alias, target)))
		return true
	})
	
	res.Write([]byte (
		`</tbody></table>
		<h2>Protocols</h2>
		<table>
		<thead>
			<tr>
			<th>scheme</th>
			<th>port</th>
			</tr>
		</thead>
		<tbody>`))
	listProtocol := func (protocol string, port int) {
		res.Write([]byte(fmt.Sprintf (
			"<tr><td>%s</td><td>%d</td></tr>",
			protocol, port)))
	}
	if rcon.config.HTTPSEnable() {
		listProtocol("https", rcon.config.HTTPSPort())
	}
	if rcon.config.HTTPEnable() {
		listProtocol("http", rcon.config.HTTPPort())
	}
	if rcon.config.GeminiEnable() {
		listProtocol("gemini", rcon.config.GeminiPort())
	}
	res.Write([]byte(`</tbody></table></body></html>`))
}

func (rcon *Rcon) serveServiceDetails (res http.ResponseWriter, req *http.Request) {
	name := req.URL.Query().Get("service")
	service := rcon.router.Service(name)
	if service == nil {
		http.NotFoundHandler().ServeHTTP(res, req)
		return
	}

	res.Header().Add("content-type", "text/html")
	res.WriteHeader(http.StatusFound)
	res.Write([]byte (
	`<!DOCTYPE html>
	<html>
	<head>
		<title>Details of ` + service.Name() + `</title>
		<style>` + css + `</style>
	</head>
	<body>
		<h1>Details of ` + service.Name() + `</h1>
		<h2>Requests</h2>
		<table>
		<thead>
			<tr>
			<th>id</th>
			<th>url</th>
			</tr>
		</thead>
		<tbody>`))

	service.OverRequests(func (id protocol.ID, url *url.URL) bool {
		res.Write([]byte(fmt.Sprintf (
			"<tr><td>%v</td><td>%v</td></tr>",
			id, url)))
		return true
	})

	res.Write([]byte(`</tbody></table></body></html>`))
}

func (rcon *Rcon) serveRouterOffline (res http.ResponseWriter, req *http.Request) {
	res.Header().Add("content-type", "text/html")
	res.WriteHeader(http.StatusNotFound)
	res.Write([]byte (
	`<!DOCTYPE html>
	<html>
	<head>
		<title>Router Offline</title>
		<style>` + css + `</style>
	</head>
	<body>
		<h1>Router Offline</h1>
		<p>The router has not been started yet.</p>
	</body>
	</html>`))
}
