// Copyright 2015 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package port

import (
	"encoding/json"
	"fmt"
	"net"
	"net/http"
	"strconv"
	"strings"

	"github.com/google/martian/parse"
)

func init() {
	parse.Register("port.Modifier", modifierFromJSON)
}

// Modifier alters the request URL and Host header to use the provided port.
// Only one of port, defaultForScheme, or remove may be specified.  Whichever is set last is the one that will take effect.
// If remove is true, remove the port from the host string ('example.com').
// If defaultForScheme is true, explicitly specify 80 for HTTP or 443 for HTTPS ('http://example.com:80'). Do nothing for a scheme that is not 'http' or 'https'.
// If port is specified, explicitly add it to the host string ('example.com:1234').
// If port is zero and the other fields are false, the request will not be modified.
type Modifier struct {
	port             int
	defaultForScheme bool
	remove           bool
}

type modifierJSON struct {
	Port             int                  `json:"port"`
	DefaultForScheme bool                 `json:"defaultForScheme"`
	Remove           bool                 `json:"remove"`
	Scope            []parse.ModifierType `json:"scope"`
}

// NewModifier returns a RequestModifier that can be configured to alter the request URL and Host header's port.
// One of DefaultPortForScheme, UsePort, or RemovePort should be called to configure this modifier.
func NewModifier() *Modifier {
	return &Modifier{}
}

// DefaultPortForScheme configures the modifier to explicitly specify 80 for HTTP or 443 for HTTPS ('http://example.com:80').
// The modifier will not modify requests with a scheme that is not 'http' or 'https'.
// This overrides any previous configuration for this modifier.
func (m *Modifier) DefaultPortForScheme() {
	m.defaultForScheme = true
	m.remove = false
}

// UsePort configures the modifier to add the specified port to the host string ('example.com:1234').
// This overrides any previous configuration for this modifier.
func (m *Modifier) UsePort(port int) {
	m.port = port
	m.remove = false
	m.defaultForScheme = false
}

// RemovePort configures the modifier to remove the port from the host string ('example.com').
// This overrides any previous configuration for this modifier.
func (m *Modifier) RemovePort() {
	m.remove = true
	m.defaultForScheme = false
}

// ModifyRequest alters the request URL and Host header to modify the port as specified.
// See docs for Modifier for details.
func (m *Modifier) ModifyRequest(req *http.Request) error {
	if m.port == 0 && !m.defaultForScheme && !m.remove {
		return nil
	}

	host := req.URL.Host
	if strings.Contains(host, ":") {
		h, _, err := net.SplitHostPort(host)
		if err != nil {
			return err
		}
		host = h
	}

	if m.remove {
		req.URL.Host = host
		req.Header.Set("Host", host)
		return nil
	}

	if m.defaultForScheme {
		switch req.URL.Scheme {
		case "http":
			hp := net.JoinHostPort(host, "80")
			req.URL.Host = hp
			req.Header.Set("Host", hp)
			return nil
		case "https":
			hp := net.JoinHostPort(host, "443")
			req.URL.Host = hp
			req.Header.Set("Host", hp)
			return nil
		default:
			// Unknown scheme, do nothing.
			return nil
		}
	}

	// Not removing or using default for the scheme, so use the provided port number.
	hp := net.JoinHostPort(host, strconv.Itoa(m.port))
	req.URL.Host = hp
	req.Header.Set("Host", hp)

	return nil
}

func modifierFromJSON(b []byte) (*parse.Result, error) {
	msg := &modifierJSON{}
	if err := json.Unmarshal(b, msg); err != nil {
		return nil, err
	}

	errMsg := fmt.Errorf("Must specify only one of port, defaultForScheme or remove")

	mod := NewModifier()
	// Check that exactly one field of port, defaultForScheme, and remove is set.
	switch {
	case msg.Port != 0:
		if msg.DefaultForScheme || msg.Remove {
			return nil, errMsg
		}
		mod.UsePort(msg.Port)
	case msg.DefaultForScheme:
		if msg.Port != 0 || msg.Remove {
			return nil, errMsg
		}
		mod.DefaultPortForScheme()
	case msg.Remove:
		if msg.Port != 0 || msg.DefaultForScheme {
			return nil, errMsg
		}
		mod.RemovePort()
	default:
		return nil, errMsg
	}

	return parse.NewResult(mod, msg.Scope)
}
