package internal

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"time"
)

// defaultBaseURL for reaching the jSON-based API-Endpoint of netcup.
const defaultBaseURL = "https://ccp.netcup.net/run/webservice/servers/endpoint.php?JSON"

// success response status.
const success = "success"

// Request wrapper as specified in netcup wiki
// needed for every request to netcup API around *Msg.
// https://www.netcup-wiki.de/wiki/CCP_API#Anmerkungen_zu_JSON-Requests
type Request struct {
	Action string      `json:"action"`
	Param  interface{} `json:"param"`
}

// LoginRequest as specified in netcup WSDL.
// https://ccp.netcup.net/run/webservice/servers/endpoint.php#login
type LoginRequest struct {
	CustomerNumber  string `json:"customernumber"`
	APIKey          string `json:"apikey"`
	APIPassword     string `json:"apipassword"`
	ClientRequestID string `json:"clientrequestid,omitempty"`
}

// LogoutRequest as specified in netcup WSDL.
// https://ccp.netcup.net/run/webservice/servers/endpoint.php#logout
type LogoutRequest struct {
	CustomerNumber  string `json:"customernumber"`
	APIKey          string `json:"apikey"`
	APISessionID    string `json:"apisessionid"`
	ClientRequestID string `json:"clientrequestid,omitempty"`
}

// UpdateDNSRecordsRequest as specified in netcup WSDL.
// https://ccp.netcup.net/run/webservice/servers/endpoint.php#updateDnsRecords
type UpdateDNSRecordsRequest struct {
	DomainName      string       `json:"domainname"`
	CustomerNumber  string       `json:"customernumber"`
	APIKey          string       `json:"apikey"`
	APISessionID    string       `json:"apisessionid"`
	ClientRequestID string       `json:"clientrequestid,omitempty"`
	DNSRecordSet    DNSRecordSet `json:"dnsrecordset"`
}

// DNSRecordSet as specified in netcup WSDL.
// needed in UpdateDNSRecordsRequest.
// https://ccp.netcup.net/run/webservice/servers/endpoint.php#Dnsrecordset
type DNSRecordSet struct {
	DNSRecords []DNSRecord `json:"dnsrecords"`
}

// InfoDNSRecordsRequest as specified in netcup WSDL.
// https://ccp.netcup.net/run/webservice/servers/endpoint.php#infoDnsRecords
type InfoDNSRecordsRequest struct {
	DomainName      string `json:"domainname"`
	CustomerNumber  string `json:"customernumber"`
	APIKey          string `json:"apikey"`
	APISessionID    string `json:"apisessionid"`
	ClientRequestID string `json:"clientrequestid,omitempty"`
}

// DNSRecord as specified in netcup WSDL.
// https://ccp.netcup.net/run/webservice/servers/endpoint.php#Dnsrecord
type DNSRecord struct {
	ID           int    `json:"id,string,omitempty"`
	Hostname     string `json:"hostname"`
	RecordType   string `json:"type"`
	Priority     string `json:"priority,omitempty"`
	Destination  string `json:"destination"`
	DeleteRecord bool   `json:"deleterecord,omitempty"`
	State        string `json:"state,omitempty"`
	TTL          int    `json:"ttl,omitempty"`
}

// ResponseMsg as specified in netcup WSDL.
// https://ccp.netcup.net/run/webservice/servers/endpoint.php#Responsemessage
type ResponseMsg struct {
	ServerRequestID string          `json:"serverrequestid"`
	ClientRequestID string          `json:"clientrequestid,omitempty"`
	Action          string          `json:"action"`
	Status          string          `json:"status"`
	StatusCode      int             `json:"statuscode"`
	ShortMessage    string          `json:"shortmessage"`
	LongMessage     string          `json:"longmessage"`
	ResponseData    json.RawMessage `json:"responsedata,omitempty"`
}

func (r *ResponseMsg) Error() string {
	return fmt.Sprintf("an error occurred during the action %s: [Status=%s, StatusCode=%d, ShortMessage=%s, LongMessage=%s]",
		r.Action, r.Status, r.StatusCode, r.ShortMessage, r.LongMessage)
}

// LoginResponse response to login action.
type LoginResponse struct {
	APISessionID string `json:"apisessionid"`
}

// InfoDNSRecordsResponse response to infoDnsRecords action.
type InfoDNSRecordsResponse struct {
	APISessionID string      `json:"apisessionid"`
	DNSRecords   []DNSRecord `json:"dnsrecords,omitempty"`
}

// Client netcup DNS client.
type Client struct {
	customerNumber string
	apiKey         string
	apiPassword    string
	HTTPClient     *http.Client
	BaseURL        string
}

// NewClient creates a netcup DNS client.
func NewClient(customerNumber, apiKey, apiPassword string) (*Client, error) {
	if customerNumber == "" || apiKey == "" || apiPassword == "" {
		return nil, errors.New("credentials missing")
	}

	return &Client{
		customerNumber: customerNumber,
		apiKey:         apiKey,
		apiPassword:    apiPassword,
		BaseURL:        defaultBaseURL,
		HTTPClient: &http.Client{
			Timeout: 10 * time.Second,
		},
	}, nil
}

// Login performs the login as specified by the netcup WSDL
// returns sessionID needed to perform remaining actions.
// https://ccp.netcup.net/run/webservice/servers/endpoint.php
func (c *Client) Login() (string, error) {
	payload := &Request{
		Action: "login",
		Param: &LoginRequest{
			CustomerNumber:  c.customerNumber,
			APIKey:          c.apiKey,
			APIPassword:     c.apiPassword,
			ClientRequestID: "",
		},
	}

	var responseData LoginResponse
	err := c.doRequest(payload, &responseData)
	if err != nil {
		return "", fmt.Errorf("loging error: %w", err)
	}

	return responseData.APISessionID, nil
}

// Logout performs the logout with the supplied sessionID as specified by the netcup WSDL.
// https://ccp.netcup.net/run/webservice/servers/endpoint.php
func (c *Client) Logout(sessionID string) error {
	payload := &Request{
		Action: "logout",
		Param: &LogoutRequest{
			CustomerNumber:  c.customerNumber,
			APIKey:          c.apiKey,
			APISessionID:    sessionID,
			ClientRequestID: "",
		},
	}

	err := c.doRequest(payload, nil)
	if err != nil {
		return fmt.Errorf("logout error: %w", err)
	}

	return nil
}

// UpdateDNSRecord performs an update of the DNSRecords as specified by the netcup WSDL.
// https://ccp.netcup.net/run/webservice/servers/endpoint.php
func (c *Client) UpdateDNSRecord(sessionID, domainName string, records []DNSRecord) error {
	payload := &Request{
		Action: "updateDnsRecords",
		Param: UpdateDNSRecordsRequest{
			DomainName:      domainName,
			CustomerNumber:  c.customerNumber,
			APIKey:          c.apiKey,
			APISessionID:    sessionID,
			ClientRequestID: "",
			DNSRecordSet:    DNSRecordSet{DNSRecords: records},
		},
	}

	err := c.doRequest(payload, nil)
	if err != nil {
		return fmt.Errorf("error when sending the request: %w", err)
	}

	return nil
}

// GetDNSRecords retrieves all dns records of an DNS-Zone as specified by the netcup WSDL
// returns an array of DNSRecords.
// https://ccp.netcup.net/run/webservice/servers/endpoint.php
func (c *Client) GetDNSRecords(hostname, apiSessionID string) ([]DNSRecord, error) {
	payload := &Request{
		Action: "infoDnsRecords",
		Param: InfoDNSRecordsRequest{
			DomainName:      hostname,
			CustomerNumber:  c.customerNumber,
			APIKey:          c.apiKey,
			APISessionID:    apiSessionID,
			ClientRequestID: "",
		},
	}

	var responseData InfoDNSRecordsResponse
	err := c.doRequest(payload, &responseData)
	if err != nil {
		return nil, fmt.Errorf("error when sending the request: %w", err)
	}

	return responseData.DNSRecords, nil
}

// doRequest marshals given body to JSON, send the request to netcup API
// and returns body of response.
func (c *Client) doRequest(payload, responseData interface{}) error {
	body, err := json.Marshal(payload)
	if err != nil {
		return err
	}

	req, err := http.NewRequest(http.MethodPost, c.BaseURL, bytes.NewReader(body))
	if err != nil {
		return err
	}

	req.Close = true
	req.Header.Set("content-type", "application/json")

	resp, err := c.HTTPClient.Do(req)
	if err != nil {
		return err
	}

	if err = checkResponse(resp); err != nil {
		return err
	}

	respMsg, err := decodeResponseMsg(resp)
	if err != nil {
		return err
	}

	if respMsg.Status != success {
		return respMsg
	}

	if responseData != nil {
		err = json.Unmarshal(respMsg.ResponseData, responseData)
		if err != nil {
			return fmt.Errorf("%v: unmarshaling %T error: %w: %s",
				respMsg, responseData, err, string(respMsg.ResponseData))
		}
	}

	return nil
}

func checkResponse(resp *http.Response) error {
	if resp.StatusCode > 299 {
		if resp.Body == nil {
			return fmt.Errorf("response body is nil, status code=%d", resp.StatusCode)
		}

		defer resp.Body.Close()

		raw, err := io.ReadAll(resp.Body)
		if err != nil {
			return fmt.Errorf("unable to read body: status code=%d, error=%w", resp.StatusCode, err)
		}

		return fmt.Errorf("status code=%d: %s", resp.StatusCode, string(raw))
	}

	return nil
}

func decodeResponseMsg(resp *http.Response) (*ResponseMsg, error) {
	if resp.Body == nil {
		return nil, fmt.Errorf("response body is nil, status code=%d", resp.StatusCode)
	}

	defer resp.Body.Close()

	raw, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("unable to read body: status code=%d, error=%w", resp.StatusCode, err)
	}

	var respMsg ResponseMsg
	err = json.Unmarshal(raw, &respMsg)
	if err != nil {
		return nil, fmt.Errorf("unmarshaling %T error [status code=%d]: %w: %s", respMsg, resp.StatusCode, err, string(raw))
	}

	return &respMsg, nil
}

// GetDNSRecordIdx searches a given array of DNSRecords for a given DNSRecord
// equivalence is determined by Destination and RecortType attributes
// returns index of given DNSRecord in given array of DNSRecords.
func GetDNSRecordIdx(records []DNSRecord, record DNSRecord) (int, error) {
	for index, element := range records {
		if record.Destination == element.Destination && record.RecordType == element.RecordType {
			return index, nil
		}
	}
	return -1, errors.New("no DNS Record found")
}
