// Main package for Tog Switchifier. See accompanying README.md file.

// Copyright (c) 2016, Serge Bazanski <s@bazanski.pl>
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE

package main

import (
	"database/sql"
	"encoding/json"
	"flag"
	"net/http"
	"strings"
	"time"

	"github.com/golang/glog"
	_ "github.com/mattn/go-sqlite3"
)

type switchApp struct {
	db         *sql.DB
	secret     string
	lastUpdate int64
}

func newSwitchApp(db *sql.DB) switchApp {
	return switchApp{
		db:     db,
		secret: secret,
	}
}

func (s *switchApp) RunSchemaUpdate() error {
	sqlStmt := `
CREATE TABLE IF NOT EXISTS switch_state_change(
	timestamp INTEGER NOT NULL PRIMARY KEY,
	interval INTEGER NOT NULL,
	state BOOLEAN NOT NULL
);`
	_, err := s.db.Exec(sqlStmt)
	return err
}

func (s *switchApp) UpdateState(state bool) error {
	defer func() {
		s.lastUpdate = time.Now().UnixNano()
	}()
	sqlStmt := `
SELECT state FROM switch_state_change
	ORDER BY timestamp DESC
	LIMIT 1;`
	res, err := s.db.Query(sqlStmt)
	if err != nil {
		return err
	}

	shouldStore := false
	if !res.Next() {
		// Always store a state if the database is empty
		shouldStore = true
	} else {
		// Otherwise store if there was a state change
		var lastState bool
		if err = res.Scan(&lastState); err != nil {
			return err
		}
		if lastState != state {
			shouldStore = true
		}
	}
	if !shouldStore {
		return nil
	}

	glog.Info("Storing state change in database...")
	timestamp := time.Now().UnixNano()
	interval := int64(0)
	if s.lastUpdate != 0 {
		interval = timestamp - s.lastUpdate
	}
	sqlStmt = `INSERT INTO switch_state_change(timestamp, interval, state) VALUES (?, ?, ?)`
	_, err = s.db.Exec(sqlStmt, timestamp, interval, state)
	return err
}

func (s *switchApp) HandleAPIUpdate(w http.ResponseWriter, r *http.Request) {
	glog.Infof("%s: %s %s", r.RemoteAddr, r.Method, r.URL.String())
	if r.Method != "POST" {
		w.WriteHeader(403)
		return
	}
	// Yes, this should be a constant-time comparison.
	if secret := r.FormValue("secret"); secret != s.secret {
		w.WriteHeader(403)
		return
	}

	newValueStr := strings.ToLower(r.FormValue("value"))
	if newValueStr == "" {
		w.WriteHeader(400)
		return
	}
	var newValue bool
	if strings.HasPrefix(newValueStr, "t") || newValueStr == "1" {
		newValue = true
	}

	glog.Infof("Switch state: %v.", newValue)
	err := s.UpdateState(newValue)
	if err != nil {
		w.WriteHeader(500)
		glog.Errorf("Error in handler: %v", err)
		return
	}
	w.WriteHeader(200)
}

type apiGetResponse struct {
	Okay  bool   `json:"okay"`
	Error string `json:"error"`
	Data  struct {
		Open          bool  `json:"open"`
		Since         int64 `json:"since"`
		LastKeepalive int64 `json:"lastKeepalive"`
	} `json:"data"`
}

func (s *switchApp) HandleAPIGet(w http.ResponseWriter, r *http.Request) {
	resp := apiGetResponse{}
	defer func() {
		if resp.Okay {
			resp.Data.LastKeepalive = s.lastUpdate
		}
		respJson, err := json.Marshal(resp)
		if err != nil {
			w.WriteHeader(500)
			w.Write([]byte(`{ okay: false, error: "Internal server error." }`))
			glog.Errorf("Error in handler: %v", err)
		}
		w.Write(respJson)
	}()

	sqlStmt := `SELECT timestamp, state FROM switch_state_change
ORDER BY timestamp DESC
LIMIT 1`
	data, err := s.db.Query(sqlStmt)
	if err != nil {
		resp.Okay = false
		resp.Error = "Database error."
		return
	}
	if !data.Next() {
		resp.Okay = false
		resp.Error = "No switch state yet."
		return
	}
	var timestamp int64
	var state bool
	if data.Scan(&timestamp, &state) != nil {
		resp.Okay = false
		resp.Error = "Database error."
		return
	}
	resp.Okay = true
	resp.Data.Open = state
	resp.Data.Since = timestamp
}

var (
	dbPath      string
	bindAddress string
	secret      string
)

func main() {
	flag.StringVar(&dbPath, "db_path", "./switchifier.db", "Path to the SQlite3 database.")
	flag.StringVar(&bindAddress, "bind_address", ":8080", "Address to bind HTTP server to.")
	flag.StringVar(&secret, "secret", "changeme", "Secret for state updates.")
	flag.Parse()
	if dbPath == "" {
		glog.Exit("Please provide a database path.")
	}
	glog.Infof("Starting switchifier...")

	db, err := sql.Open("sqlite3", dbPath)
	if err != nil {
		glog.Exit(err)
	}
	defer db.Close()

	switchApp := newSwitchApp(db)
	if err = switchApp.RunSchemaUpdate(); err != nil {
		glog.Exitf("Could not run schema update: %v", err)
	}
	glog.Info("Schema updates applied.")

	http.HandleFunc("/api/1/switchifier/update", switchApp.HandleAPIUpdate)
	http.HandleFunc("/api/1/switchifier/status", switchApp.HandleAPIGet)

	glog.Infof("Listening on %s.", bindAddress)
	glog.Exit(http.ListenAndServe(bindAddress, nil))
}
