/*
 Copyright 2013-2014 Canonical Ltd.

 This program is free software: you can redistribute it and/or modify it
 under the terms of the GNU General Public License version 3, as published
 by the Free Software Foundation.

 This program is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranties of
 MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
 PURPOSE.  See the GNU General Public License for more details.

 You should have received a copy of the GNU General Public License along
 with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

package service

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	"os"

	"github.com/godbus/dbus/v5"

	"gitlab.com/ubports/development/core/lomiri-push-service/bus"
	"gitlab.com/ubports/development/core/lomiri-push-service/click"
	"gitlab.com/ubports/development/core/lomiri-push-service/logger"
	"gitlab.com/ubports/development/core/lomiri-push-service/nih"
)

// PushServiceSetup encapsulates the params for setting up a PushService.
type PushServiceSetup struct {
	RegURL           *url.URL
	DeviceId         string
	InstalledChecker click.InstalledChecker
}

// PushService is the dbus api
type PushService struct {
	DBusService
	regURL     *url.URL
	deviceId   string
	httpCli    http.Client
}

var (
	PushServiceBusAddress = bus.Address{
		Interface: "com.lomiri.PushNotifications",
		Path:      "/com/lomiri/PushNotifications",
		Name:      "com.lomiri.PushNotifications",
	}
)

// NewPushService() builds a new service and returns it.
func NewPushService(setup *PushServiceSetup, log logger.Logger) *PushService {
	var svc = &PushService{}
	svc.Log = log
	svc.Bus = bus.SessionBus.Endpoint(PushServiceBusAddress, log)
	svc.installedChecker = setup.InstalledChecker
	svc.regURL = setup.RegURL
	svc.deviceId = setup.DeviceId
	return svc
}

// getParsedUrl() returns the POST URL at registration HTTP endpoint for op
func (svc *PushService) getParsedUrl(op string) (string) {
	if svc.regURL == nil {
		return ""
	}
	purl, err := svc.regURL.Parse(op)
	if err != nil {
		panic("op to getParsedUrl was invalid")
	}
	url := purl.String()
	return url
}

func (svc *PushService) Start() error {
	return svc.DBusService.Start(bus.DispatchMap{
		"Register":   svc.register,
		"Unregister": svc.unregister,
	}, PushServiceBusAddress, nil)
}

var (
	ErrBadServer  = errors.New("bad server")
	ErrBadRequest = errors.New("bad request")
	ErrBadToken   = errors.New("bad token")
	ErrBadAuth    = errors.New("bad auth")
)

type registrationRequest struct {
	DeviceId string `json:"deviceid"`
	AppId    string `json:"appid"`
}

type registrationReply struct {
	Token   string `json:"token"`   // the bit we're after
	Ok      bool   `json:"ok"`      // only ever true or absent
	Error   string `json:"error"`   // these two only used for debugging
	Message string `json:"message"` //
}

func (svc *PushService) manageReg(op, appId string) (*registrationReply, error) {
	req_body, err := json.Marshal(registrationRequest{svc.deviceId, appId})
	if err != nil {
		return nil, fmt.Errorf("unable to marshal register request body: %v", err)
	}

	url := svc.getParsedUrl(op)

	req, err := http.NewRequest("POST", url, bytes.NewReader(req_body))
	if err != nil {
		panic(fmt.Errorf("unable to build register request: %v", err))
	}
	req.Header.Add("Content-Type", "application/json")

	resp, err := svc.httpCli.Do(req)
	if err != nil {
		return nil, fmt.Errorf("unable to request registration: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		svc.Log.Errorf("register endpoint replied %d", resp.StatusCode)
		switch {
		case resp.StatusCode >= http.StatusInternalServerError:
			// XXX retry on 503
			return nil, ErrBadServer
		case resp.StatusCode == http.StatusUnauthorized:
			return nil, ErrBadAuth
		default:
			return nil, ErrBadRequest
		}
	}
	// errors below here Can't Happen (tm).
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		svc.Log.Errorf("during ReadAll() of response body: %v", err)
		return nil, err
	}

	var reply registrationReply
	err = json.Unmarshal(body, &reply)
	if err != nil {
		svc.Log.Errorf("during Unmarshal of response body: %v", err)
		return nil, fmt.Errorf("unable to unmarshal register response: %v", err)
	}

	return &reply, nil
}

func (svc *PushService) register(msg dbus.Message, id string) (string, *dbus.Error) {
	app, err := svc.grabDBusPackageAndAppId(&msg, id)
	if err != nil {
		return "", MakeDBusNoObjectErrorWithReason(
			msg.Headers[dbus.FieldPath].Value().(string), err)
	}

	rawAppId := string(nih.Quote([]byte(app.Original())))
	rv := os.Getenv("PUSH_REG_" + rawAppId)
	if rv != "" {
		return rv, nil
	}

	reply, err := svc.manageReg("/register", app.Original())
	if err != nil {
		return "", dbus.MakeFailedError(err)
	}

	if !reply.Ok || reply.Token == "" {
		svc.Log.Errorf("unexpected response: %#v", reply)
		return "", dbus.MakeFailedError(ErrBadToken)
	}

	return reply.Token, nil
}

func (svc *PushService) unregister(msg dbus.Message, id string) *dbus.Error {
	app, err := svc.grabDBusPackageAndAppId(&msg, id)
	if err != nil {
		return MakeDBusNoObjectErrorWithReason(
			msg.Headers[dbus.FieldPath].Value().(string), err)
	}

	err = svc.Unregister(app.Original())
	if err != nil {
		dbus.MakeFailedError(err)
	}
	return nil
}

func (svc *PushService) Unregister(appId string) error {
	_, err := svc.manageReg("/unregister", appId)
	return err
}
