// Package netboxapi provides custom functions for the default
// go-netbox package. It mostly handles pagination.
package netboxapi

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"

	e "git.stinnesbeck.com/nils/errorhandler"
	transport "github.com/go-openapi/runtime/client"
	"github.com/netbox-community/go-netbox/v3/netbox/client"
)

// NetBoxAPI is the type we use to add custom functions
type NetBoxAPI struct {
	api   client.NetBoxAPI
	Token string
	URL   string
}

type apiResponse struct {
	Count    int64  `json:"count"`
	Next     string `json:"next"`
	Previous string `json:"previous"`
	Results  []any  `json:"results"`
}

// NewNetBoxClient returns a client to NetBox at the given url,
// using the provided token
func NewNetBoxClient(token string, url string) (NetBoxAPI, error) {
	// Construct the API basis and configure authentication
	t := transport.New(url, client.DefaultBasePath, []string{"https"})
	t.DefaultAuthentication = transport.APIKeyAuth(
		"Authorization",
		"header",
		fmt.Sprintf("Token %v", token),
	)

	// create NetBox api variable
	n := client.New(t, nil)

	// create a new variable for our custom functions
	nb := NetBoxAPI{
		api:   *n,
		Token: token,
		URL:   url,
	}
	return nb, nil
}

func (nb NetBoxAPI) handleRawPagination(url string) (*apiResponse, error) {
	var response apiResponse
	b, err := nb.rawNetBoxAPICall(url, http.MethodGet)
	if err != nil {
		return nil, e.FormatError("can't perform raw NetBox API call", err)
	}

	// Unmarshal json into response
	if err := json.Unmarshal(b, &response); err != nil {
		return nil, e.FormatError("can't unmarshal json payload in raw NetBox API call", err)
	}

	// there are no more pages left, return the response we got
	if response.Next == "" {
		return &response, nil
	}

	// there are more pages left, get more responses from there

	// recursive calling of handlePagination to get all following pages
	nextResponse, err := nb.handleRawPagination(response.Next)
	if err != nil {
		return nil, e.FormatError("can't get next response from api", err)
	}

	// append all next responses' results to this response's result
	response.Results = append(response.Results, nextResponse.Results...)

	// go back up the chain through recursion
	return &response, nil
}

func (nb NetBoxAPI) handlePagination(url string, payload any) error {
	response, err := nb.handleRawPagination(url)
	if err != nil {
		return e.FormatError("can't handle pagination for url "+url, err)
	}

	fmt.Println(response.Count)

	// cast response into our payload
	if err := resultToOtherStructure(*response, &payload); err != nil {
		return e.FormatError("can't parse result to other Structure", err)
	}

	// prettify.Print(payload)
	return nil

}

func resultToOtherStructure(input apiResponse, output any) error {
	// get JSON representation of results
	c, err := json.Marshal(input.Results)
	if err != nil {
		return err
	}

	// Unmarshal onto specific structure via output
	return json.Unmarshal(c, &output)
}

func (nb NetBoxAPI) rawNetBoxAPICall(url string, method string, data ...any) ([]byte, error) {
	// construct complete URL (url already comes with a "/" after the "/api" part)
	// url = fmt.Sprintf("https://%s/api%s", nb.URL, url)

	// log.Printf("calling API on url: %s with method %s and data %+v\n", url, method, data)
	// create new transport client

	c := &http.Client{}
	var req *http.Request

	switch method {
	case http.MethodPatch, http.MethodPost, http.MethodPut:
		// to patch something we need data, check it exists
		if data == nil {
			return nil, fmt.Errorf("no data was provided, please provide data to use PATCH")
		}

		payload, err := json.Marshal(data)
		if err != nil {
			return nil, e.FormatError(fmt.Sprintf("can't marshal data %+v for apicall %s", data, url), err)
		}

		// construct request
		innerRequest, err := http.NewRequest(method, url, bytes.NewBuffer(payload))
		if err != nil {
			return nil, e.FormatError(fmt.Sprintf("can't construct new request (method: %s, URL: %s, payload %s)", method, url, payload), err)
		}

		// hand over request to outer request
		req = innerRequest

	case http.MethodGet:
		innerRequest, err := http.NewRequest(method, url, nil)
		if err != nil {
			return nil, e.FormatError(fmt.Sprintf("can't construct new request (method: %s, URL: %s)", method, url), err)
		}

		// hand over request to outer request
		req = innerRequest
	}

	// set request headers
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", fmt.Sprintf("Token %s", nb.Token))

	// call API client with request
	resp, err := c.Do(req)
	if err != nil {
		return nil, e.FormatError("can't get response from "+url, err)
	}
	defer resp.Body.Close()

	// read body of response
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, e.FormatError("can't read response body", err)
	}

	switch resp.StatusCode {
	case 200, 201:
		return body, nil
	default:
		return body, fmt.Errorf("got bad StatusCode %d in response from API, data is:\n\t-> %+v", resp.StatusCode, data)
	}
}