package gemini

import (
	"bufio"
	"bytes"
	"errors"
	"io"
	"strconv"
	"sync"

	"tildegit.org/tjp/sliderule/internal/types"
)

// ResponseCategory represents the various types of gemini responses.
type ResponseCategory int

const (
	// ResponseCategoryInput is for responses which request additional input.
	//
	// The META line will be the prompt to display to the user.
	ResponseCategoryInput ResponseCategory = iota*10 + 10
	// ResponseCategorySuccess is for successful responses.
	//
	// The META line will be the resource's mime type.
	// This is the only response status which indicates the presence of a response body,
	// and it will contain the resource itself.
	ResponseCategorySuccess
	// ResponseCategoryRedirect is for responses which direct the client to an alternative URL.
	//
	// The META line will contain the new URL the client should try.
	ResponseCategoryRedirect
	// ResponseCategoryTemporaryFailure is for responses which indicate a transient server-side failure.
	//
	// The META line may contain a line with more information about the error.
	ResponseCategoryTemporaryFailure
	// ResponseCategoryPermanentFailure is for permanent failure responses.
	//
	// The META line may contain a line with more information about the error.
	ResponseCategoryPermanentFailure
	// ResponseCategoryCertificateRequired indicates client certificate related issues.
	//
	// The META line may contain a line with more information about the error.
	ResponseCategoryCertificateRequired
)

func ResponseCategoryForStatus(status types.Status) ResponseCategory {
	return ResponseCategory((status / 10) * 10)
}

const (
	// StatusInput indicates a required query parameter at the requested URL.
	StatusInput types.Status = types.Status(ResponseCategoryInput) + iota
	// StatusSensitiveInput indicates a sensitive query parameter is required.
	StatusSensitiveInput
)

const (
	// StatusSuccess is a successful response.
	StatusSuccess = types.Status(ResponseCategorySuccess) + iota
)

const (
	// StatusTemporaryRedirect indicates a temporary redirect to another URL.
	StatusTemporaryRedirect = types.Status(ResponseCategoryRedirect) + iota
	// StatusPermanentRedirect indicates that the resource should always be requested at the new URL.
	StatusPermanentRedirect
)

const (
	// StatusTemporaryFailure indicates that the request failed and there is no response body.
	StatusTemporaryFailure = types.Status(ResponseCategoryTemporaryFailure) + iota
	// StatusServerUnavailable occurs when the server is unavailable due to overload or maintenance.
	StatusServerUnavailable
	// StatusCGIError is the result of a failure of a CGI script.
	StatusCGIError
	// StatusProxyError indicates that the server is acting as a proxy and the outbound request failed.
	StatusProxyError
	// StatusSlowDown tells the client that rate limiting is in effect.
	//
	// Unlike other statuses in this category, the META line is an integer indicating how
	// many more seconds the client must wait before sending another request.
	StatusSlowDown
)

const (
	// StatusPermanentFailure is a server failure which should be expected to continue indefinitely.
	StatusPermanentFailure = types.Status(ResponseCategoryPermanentFailure) + iota
	// StatusNotFound means the resource doesn't exist but it may in the future.
	StatusNotFound
	// StatusGone occurs when a resource will not be available any longer.
	StatusGone
	// StatusProxyRequestRefused means the server is unwilling to act as a proxy for the resource.
	StatusProxyRequestRefused
	// StatusBadRequest indicates that the request was malformed somehow.
	StatusBadRequest = types.Status(ResponseCategoryPermanentFailure) + 9
)

const (
	// StatusClientCertificateRequired is returned when a certificate was required but not provided.
	StatusClientCertificateRequired = types.Status(ResponseCategoryCertificateRequired) + iota
	// StatusCertificateNotAuthorized means the certificate doesn't grant access to the requested resource.
	StatusCertificateNotAuthorized
	// StatusCertificateNotValid means the provided client certificate is invalid.
	StatusCertificateNotValid
)

// Input builds an input-prompting response.
func Input(prompt string) *types.Response {
	return &types.Response{
		Status: StatusInput,
		Meta:   prompt,
	}
}

// SensitiveInput builds a password-prompting response.
func SensitiveInput(prompt string) *types.Response {
	return &types.Response{
		Status: StatusSensitiveInput,
		Meta:   prompt,
	}
}

// Success builds a success response with resource body.
func Success(mediatype string, body io.Reader) *types.Response {
	return &types.Response{
		Status: StatusSuccess,
		Meta:   mediatype,
		Body:   body,
	}
}

// Redirect builds a redirect response.
func Redirect(url string) *types.Response {
	return &types.Response{
		Status: StatusTemporaryRedirect,
		Meta:   url,
	}
}

// PermanentRedirect builds a response with a permanent redirect.
func PermanentRedirect(url string) *types.Response {
	return &types.Response{
		Status: StatusPermanentRedirect,
		Meta:   url,
	}
}

// Failure builds a temporary failure response from an error.
func Failure(err error) *types.Response {
	return &types.Response{
		Status: StatusTemporaryFailure,
		Meta:   err.Error(),
	}
}

// Unavailable build a "server unavailable" response.
func Unavailable(msg string) *types.Response {
	return &types.Response{
		Status: StatusServerUnavailable,
		Meta:   msg,
	}
}

// CGIError builds a "cgi error" response.
func CGIError(err string) *types.Response {
	return &types.Response{
		Status: StatusCGIError,
		Meta:   err,
	}
}

// ProxyError builds a proxy error response.
func ProxyError(msg string) *types.Response {
	return &types.Response{
		Status: StatusProxyError,
		Meta:   msg,
	}
}

// SlowDown builds a "slow down" response with the number of seconds until the resource is available.
func SlowDown(seconds int) *types.Response {
	return &types.Response{
		Status: StatusSlowDown,
		Meta:   strconv.Itoa(seconds),
	}
}

// PermanentFailure builds a "permanent failure" from an error.
func PermanentFailure(err error) *types.Response {
	return &types.Response{
		Status: StatusPermanentFailure,
		Meta:   err.Error(),
	}
}

// NotFound builds a "resource not found" response.
func NotFound(msg string) *types.Response {
	return &types.Response{
		Status: StatusNotFound,
		Meta:   msg,
	}
}

// Gone builds a "resource gone" response.
func Gone(msg string) *types.Response {
	return &types.Response{
		Status: StatusGone,
		Meta:   msg,
	}
}

// RefuseProxy builds a "proxy request refused" response.
func RefuseProxy(msg string) *types.Response {
	return &types.Response{
		Status: StatusProxyRequestRefused,
		Meta:   msg,
	}
}

// BadRequest builds a "bad request" response.
func BadRequest(msg string) *types.Response {
	return &types.Response{
		Status: StatusBadRequest,
		Meta:   msg,
	}
}

// RequireCert builds a "client certificate required" response.
func RequireCert(msg string) *types.Response {
	return &types.Response{
		Status: StatusClientCertificateRequired,
		Meta:   msg,
	}
}

// CertAuthFailure builds a "certificate not authorized" response.
func CertAuthFailure(msg string) *types.Response {
	return &types.Response{
		Status: StatusCertificateNotAuthorized,
		Meta:   msg,
	}
}

// CertInvalid builds a "client certificate not valid" response.
func CertInvalid(msg string) *types.Response {
	return &types.Response{
		Status: StatusCertificateNotValid,
		Meta:   msg,
	}
}

// InvalidResponseLineEnding indicates that a gemini response header didn't end with "\r\n".
var InvalidResponseLineEnding = errors.New("Invalid response line ending.")

// InvalidResponseHeaderLine indicates a malformed gemini response header line.
var InvalidResponseHeaderLine = errors.New("Invalid response header line.")

// ParseResponse parses a complete gemini response from a reader.
//
// The reader must contain only one gemini response.
func ParseResponse(rdr io.Reader) (*types.Response, error) {
	bufrdr := bufio.NewReader(rdr)

	hdrLine, err := bufrdr.ReadBytes('\n')
	if err != nil {
		return nil, InvalidResponseLineEnding
	}
	if hdrLine[len(hdrLine)-2] != '\r' {
		return nil, InvalidResponseLineEnding
	}
	if hdrLine[2] != ' ' {
		return nil, InvalidResponseHeaderLine
	}
	hdrLine = hdrLine[:len(hdrLine)-2]

	status, err := strconv.Atoi(string(hdrLine[:2]))
	if err != nil {
		return nil, InvalidResponseHeaderLine
	}

	return &types.Response{
		Status: types.Status(status),
		Meta:   string(hdrLine[3:]),
		Body:   bufrdr,
	}, nil
}

func NewResponseReader(response *types.Response) types.ResponseReader {
	return &responseReader{
		Response: response,
		once:     &sync.Once{},
	}
}

type responseReader struct {
	*types.Response
	reader io.Reader
	once   *sync.Once
}

func (rdr *responseReader) Read(b []byte) (int, error) {
	rdr.ensureReader()
	return rdr.reader.Read(b)
}

func (rdr *responseReader) WriteTo(dst io.Writer) (int64, error) {
	rdr.ensureReader()
	return rdr.reader.(io.WriterTo).WriteTo(dst)
}

func (rdr *responseReader) ensureReader() {
	rdr.once.Do(func() {
		hdr := bytes.NewBuffer(rdr.headerLine())
		if rdr.Body != nil {
			rdr.reader = io.MultiReader(hdr, rdr.Body)
		} else {
			rdr.reader = hdr
		}
	})
}

func (rdr responseReader) headerLine() []byte {
	meta := rdr.Meta.(string)
	buf := make([]byte, len(meta)+5)
	_ = strconv.AppendInt(buf[:0], int64(rdr.Status), 10)
	buf[2] = ' '
	copy(buf[3:], meta)
	buf[len(buf)-2] = '\r'
	buf[len(buf)-1] = '\n'
	return buf
}