package subnets

import (
	"encoding/json"
	"time"

	"github.com/gophercloud/gophercloud/v2"
	"github.com/gophercloud/gophercloud/v2/pagination"
)

type commonResult struct {
	gophercloud.Result
}

// Extract is a function that accepts a result and extracts a subnet resource.
func (r commonResult) Extract() (*Subnet, error) {
	var s struct {
		Subnet *Subnet `json:"subnet"`
	}
	err := r.ExtractInto(&s)
	return s.Subnet, err
}

// CreateResult represents the result of a create operation. Call its Extract
// method to interpret it as a Subnet.
type CreateResult struct {
	commonResult
}

// GetResult represents the result of a get operation. Call its Extract
// method to interpret it as a Subnet.
type GetResult struct {
	commonResult
}

// UpdateResult represents the result of an update operation. Call its Extract
// method to interpret it as a Subnet.
type UpdateResult struct {
	commonResult
}

// DeleteResult represents the result of a delete operation. Call its
// ExtractErr method to determine if the request succeeded or failed.
type DeleteResult struct {
	gophercloud.ErrResult
}

// AllocationPool represents a sub-range of cidr available for dynamic
// allocation to ports, e.g. {Start: "10.0.0.2", End: "10.0.0.254"}
type AllocationPool struct {
	Start string `json:"start"`
	End   string `json:"end"`
}

// HostRoute represents a route that should be used by devices with IPs from
// a subnet (not including local subnet route).
type HostRoute struct {
	DestinationCIDR string `json:"destination"`
	NextHop         string `json:"nexthop"`
}

// Subnet represents a subnet. See package documentation for a top-level
// description of what this is.
type Subnet struct {
	// UUID representing the subnet.
	ID string `json:"id"`

	// UUID of the parent network.
	NetworkID string `json:"network_id"`

	// Human-readable name for the subnet. Might not be unique.
	Name string `json:"name"`

	// Description for the subnet.
	Description string `json:"description"`

	// IP version, either `4' or `6'.
	IPVersion int `json:"ip_version"`

	// CIDR representing IP range for this subnet, based on IP version.
	CIDR string `json:"cidr"`

	// Default gateway used by devices in this subnet.
	GatewayIP string `json:"gateway_ip"`

	// DNS name servers used by hosts in this subnet.
	DNSNameservers []string `json:"dns_nameservers"`

	//  Specifies whether the fixed IP addresses are published to the DNS.
	DNSPublishFixedIP bool `json:"dns_publish_fixed_ip"`

	// Service types associated with the subnet.
	ServiceTypes []string `json:"service_types"`

	// Sub-ranges of CIDR available for dynamic allocation to ports.
	// See AllocationPool.
	AllocationPools []AllocationPool `json:"allocation_pools"`

	// Routes that should be used by devices with IPs from this subnet
	// (not including local subnet route).
	HostRoutes []HostRoute `json:"host_routes"`

	// Specifies whether DHCP is enabled for this subnet or not.
	EnableDHCP bool `json:"enable_dhcp"`

	// TenantID is the project owner of the subnet.
	TenantID string `json:"tenant_id"`

	// ProjectID is the project owner of the subnet.
	ProjectID string `json:"project_id"`

	// The IPv6 address modes specifies mechanisms for assigning IPv6 IP addresses.
	IPv6AddressMode string `json:"ipv6_address_mode"`

	// The IPv6 router advertisement specifies whether the networking service
	// should transmit ICMPv6 packets.
	IPv6RAMode string `json:"ipv6_ra_mode"`

	// SubnetPoolID is the id of the subnet pool associated with the subnet.
	SubnetPoolID string `json:"subnetpool_id"`

	// Tags optionally set via extensions/attributestags
	Tags []string `json:"tags"`

	// RevisionNumber optionally set via extensions/standard-attr-revisions
	RevisionNumber int `json:"revision_number"`

	// SegmentID of a network segment the subnet is associated with. It is
	// available when segment extension is enabled.
	SegmentID string `json:"segment_id"`

	// Timestamp when the subnet was created
	CreatedAt time.Time `json:"-"`

	// Timestamp when the subnet was last updated
	UpdatedAt time.Time `json:"-"`
}

func (r *Subnet) UnmarshalJSON(b []byte) error {
	type tmp Subnet

	// Support for older neutron time format
	var s1 struct {
		tmp
		CreatedAt gophercloud.JSONRFC3339NoZ `json:"created_at"`
		UpdatedAt gophercloud.JSONRFC3339NoZ `json:"updated_at"`
	}

	err := json.Unmarshal(b, &s1)
	if err == nil {
		*r = Subnet(s1.tmp)
		r.CreatedAt = time.Time(s1.CreatedAt)
		r.UpdatedAt = time.Time(s1.UpdatedAt)

		return nil
	}

	// Support for newer neutron time format
	var s2 struct {
		tmp
		CreatedAt time.Time `json:"created_at"`
		UpdatedAt time.Time `json:"updated_at"`
	}

	err = json.Unmarshal(b, &s2)
	if err != nil {
		return err
	}

	*r = Subnet(s2.tmp)
	r.CreatedAt = time.Time(s2.CreatedAt)
	r.UpdatedAt = time.Time(s2.UpdatedAt)

	return nil
}

// SubnetPage is the page returned by a pager when traversing over a collection
// of subnets.
type SubnetPage struct {
	pagination.LinkedPageBase
}

// NextPageURL is invoked when a paginated collection of subnets has reached
// the end of a page and the pager seeks to traverse over a new one. In order
// to do this, it needs to construct the next page's URL.
func (r SubnetPage) NextPageURL() (string, error) {
	var s struct {
		Links []gophercloud.Link `json:"subnets_links"`
	}
	err := r.ExtractInto(&s)
	if err != nil {
		return "", err
	}
	return gophercloud.ExtractNextURL(s.Links)
}

// IsEmpty checks whether a SubnetPage struct is empty.
func (r SubnetPage) IsEmpty() (bool, error) {
	if r.StatusCode == 204 {
		return true, nil
	}

	is, err := ExtractSubnets(r)
	return len(is) == 0, err
}

// ExtractSubnets accepts a Page struct, specifically a SubnetPage struct,
// and extracts the elements into a slice of Subnet structs. In other words,
// a generic collection is mapped into a relevant slice.
func ExtractSubnets(r pagination.Page) ([]Subnet, error) {
	var s struct {
		Subnets []Subnet `json:"subnets"`
	}
	err := (r.(SubnetPage)).ExtractInto(&s)
	return s.Subnets, err
}
