package cfapi

import (
	"encoding/json"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"path"
	"time"

	"github.com/google/uuid"
	"github.com/pkg/errors"
)

// Route is a mapping from customer's IP space to a tunnel.
// Each route allows the customer to route eyeballs in their corporate network
// to certain private IP ranges. Each Route represents an IP range in their
// network, and says that eyeballs can reach that route using the corresponding
// tunnel.
type Route struct {
	Network  CIDR      `json:"network"`
	TunnelID uuid.UUID `json:"tunnel_id"`
	// Optional field. When unset, it means the Route belongs to the default virtual network.
	VNetID    *uuid.UUID `json:"virtual_network_id,omitempty"`
	Comment   string     `json:"comment"`
	CreatedAt time.Time  `json:"created_at"`
	DeletedAt time.Time  `json:"deleted_at"`
}

// CIDR is just a newtype wrapper around net.IPNet. It adds JSON unmarshalling.
type CIDR net.IPNet

func (c CIDR) String() string {
	n := net.IPNet(c)
	return n.String()
}

func (c CIDR) MarshalJSON() ([]byte, error) {
	str := c.String()
	json, err := json.Marshal(str)
	if err != nil {
		return nil, errors.Wrap(err, "error serializing CIDR into JSON")
	}
	return json, nil
}

// UnmarshalJSON parses a JSON string into net.IPNet
func (c *CIDR) UnmarshalJSON(data []byte) error {
	var s string
	if err := json.Unmarshal(data, &s); err != nil {
		return errors.Wrap(err, "error parsing cidr string")
	}
	_, network, err := net.ParseCIDR(s)
	if err != nil {
		return errors.Wrap(err, "error parsing invalid network from backend")
	}
	if network == nil {
		return fmt.Errorf("backend returned invalid network %s", s)
	}
	*c = CIDR(*network)
	return nil
}

// NewRoute has all the parameters necessary to add a new route to the table.
type NewRoute struct {
	Network  net.IPNet
	TunnelID uuid.UUID
	Comment  string
	// Optional field. If unset, backend will assume the default vnet for the account.
	VNetID *uuid.UUID
}

// MarshalJSON handles fields with non-JSON types (e.g. net.IPNet).
func (r NewRoute) MarshalJSON() ([]byte, error) {
	return json.Marshal(&struct {
		Network  string     `json:"network"`
		TunnelID uuid.UUID  `json:"tunnel_id"`
		Comment  string     `json:"comment"`
		VNetID   *uuid.UUID `json:"virtual_network_id,omitempty"`
	}{
		Network:  r.Network.String(),
		TunnelID: r.TunnelID,
		Comment:  r.Comment,
		VNetID:   r.VNetID,
	})
}

// DetailedRoute is just a Route with some extra fields, e.g. TunnelName.
type DetailedRoute struct {
	ID       uuid.UUID `json:"id"`
	Network  CIDR      `json:"network"`
	TunnelID uuid.UUID `json:"tunnel_id"`
	// Optional field. When unset, it means the DetailedRoute belongs to the default virtual network.
	VNetID     *uuid.UUID `json:"virtual_network_id,omitempty"`
	Comment    string     `json:"comment"`
	CreatedAt  time.Time  `json:"created_at"`
	DeletedAt  time.Time  `json:"deleted_at"`
	TunnelName string     `json:"tunnel_name"`
}

// IsZero checks if DetailedRoute is the zero value.
func (r *DetailedRoute) IsZero() bool {
	return r.TunnelID == uuid.Nil
}

// TableString outputs a table row summarizing the route, to be used
// when showing the user their routing table.
func (r DetailedRoute) TableString() string {
	deletedColumn := "-"
	if !r.DeletedAt.IsZero() {
		deletedColumn = r.DeletedAt.Format(time.RFC3339)
	}
	vnetColumn := "default"
	if r.VNetID != nil {
		vnetColumn = r.VNetID.String()
	}

	return fmt.Sprintf(
		"%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t",
		r.ID,
		r.Network.String(),
		vnetColumn,
		r.Comment,
		r.TunnelID,
		r.TunnelName,
		r.CreatedAt.Format(time.RFC3339),
		deletedColumn,
	)
}

type GetRouteByIpParams struct {
	Ip net.IP
	// Optional field. If unset, backend will assume the default vnet for the account.
	VNetID *uuid.UUID
}

// ListRoutes calls the Tunnelstore GET endpoint for all routes under an account.
func (r *RESTClient) ListRoutes(filter *IpRouteFilter) ([]*DetailedRoute, error) {
	endpoint := r.baseEndpoints.accountRoutes
	endpoint.RawQuery = filter.Encode()
	resp, err := r.sendRequest("GET", endpoint, nil)
	if err != nil {
		return nil, errors.Wrap(err, "REST request failed")
	}
	defer resp.Body.Close()

	if resp.StatusCode == http.StatusOK {
		return parseListDetailedRoutes(resp.Body)
	}

	return nil, r.statusCodeToError("list routes", resp)
}

// AddRoute calls the Tunnelstore POST endpoint for a given route.
func (r *RESTClient) AddRoute(newRoute NewRoute) (Route, error) {
	endpoint := r.baseEndpoints.accountRoutes
	endpoint.Path = path.Join(endpoint.Path)
	resp, err := r.sendRequest("POST", endpoint, newRoute)
	if err != nil {
		return Route{}, errors.Wrap(err, "REST request failed")
	}
	defer resp.Body.Close()

	if resp.StatusCode == http.StatusOK {
		return parseRoute(resp.Body)
	}

	return Route{}, r.statusCodeToError("add route", resp)
}

// DeleteRoute calls the Tunnelstore DELETE endpoint for a given route.
func (r *RESTClient) DeleteRoute(id uuid.UUID) error {
	endpoint := r.baseEndpoints.accountRoutes
	endpoint.Path = path.Join(endpoint.Path, url.PathEscape(id.String()))

	resp, err := r.sendRequest("DELETE", endpoint, nil)
	if err != nil {
		return errors.Wrap(err, "REST request failed")
	}
	defer resp.Body.Close()

	if resp.StatusCode == http.StatusOK {
		_, err := parseRoute(resp.Body)
		return err
	}

	return r.statusCodeToError("delete route", resp)
}

// GetByIP checks which route will proxy a given IP.
func (r *RESTClient) GetByIP(params GetRouteByIpParams) (DetailedRoute, error) {
	endpoint := r.baseEndpoints.accountRoutes
	endpoint.Path = path.Join(endpoint.Path, "ip", url.PathEscape(params.Ip.String()))
	setVnetParam(&endpoint, params.VNetID)

	resp, err := r.sendRequest("GET", endpoint, nil)
	if err != nil {
		return DetailedRoute{}, errors.Wrap(err, "REST request failed")
	}
	defer resp.Body.Close()

	if resp.StatusCode == http.StatusOK {
		return parseDetailedRoute(resp.Body)
	}

	return DetailedRoute{}, r.statusCodeToError("get route by IP", resp)
}

func parseListDetailedRoutes(body io.ReadCloser) ([]*DetailedRoute, error) {
	var routes []*DetailedRoute
	err := parseResponse(body, &routes)
	return routes, err
}

func parseRoute(body io.ReadCloser) (Route, error) {
	var route Route
	err := parseResponse(body, &route)
	return route, err
}

func parseDetailedRoute(body io.ReadCloser) (DetailedRoute, error) {
	var route DetailedRoute
	err := parseResponse(body, &route)
	return route, err
}

// setVnetParam overwrites the URL's query parameters with a query param to scope the HostnameRoute action to a certain
// virtual network (if one is provided).
func setVnetParam(endpoint *url.URL, vnetID *uuid.UUID) {
	queryParams := url.Values{}
	if vnetID != nil {
		queryParams.Set("virtual_network_id", vnetID.String())
	}
	endpoint.RawQuery = queryParams.Encode()
}
