package httpresponse

import (
	"encoding/json"
	"errors"
	"io"
	"net/http"

	"github.com/jackc/pgx/v5/pgconn"
)

// This file is the response boundary for two classes of error that reach a
// handler without being matched against a sentinel:
//
//   - a PostgreSQL driver error, which is returned by the repository layer
//     when a write is refused. Its text is written for a server operator —
//     "new row for relation \"employees\" violates check constraint
//     \"employees_department_check\" (SQLSTATE 23514)" — and it names the
//     table, the constraint, the column and the SQLSTATE, none of which the
//     caller can act on and all of which the API contract does not promise.
//   - a JSON body that would not decode, whose text is written in terms of Go
//     — "json: cannot unmarshal array into Go value of type
//     partners.CustomerInput" — which hands the caller a Go type and a
//     package path for a request that is merely malformed.
//
// Both are answered with a message the caller can act on, and never with the
// driver's own words. The withheld detail is logged, not discarded: without
// it an operator sees only "validation error" and cannot find the rule.

// dbSafeResponse classifies err for the client. ok is false when err is not a
// database driver error, in which case the caller handles it as it always has.
func dbSafeResponse(err error) (status int, code ErrorCode, message string, ok bool) {
	var pge *pgconn.PgError
	if !errors.As(err, &pge) {
		return 0, "", "", false
	}
	if msg, isData := dataViolationMessage(pge.Code); isData {
		// The caller sent a value the server refused: that is a 4xx, whatever
		// status the handler had defaulted to.
		return http.StatusBadRequest, CodeValidation, msg, true
	}
	// Everything else from the driver — a dropped connection, a permission
	// failure, a serialization abort — is the server's fault, not the
	// caller's, and the generic internal message is all there is to say.
	return http.StatusInternalServerError, CodeInternal, MsgInternal, true
}

// dataViolationMessage maps a SQLSTATE meaning "the value submitted was
// refused" to a message the caller can act on.
//
// No entry quotes a table, column, constraint or SQLSTATE back, because those
// are the schema detail being withheld. Nothing is derived from the constraint
// name either: a CHECK violation arrives without the offending column, so the
// server does not know which field the caller got wrong, and a message naming
// a guessed field would be worse than a general one — the caller trusts it.
func dataViolationMessage(sqlstate string) (message string, ok bool) {
	switch sqlstate {
	case "23505": // unique_violation
		// Deliberately not "that value is already in use". A unique violation
		// does not mean the CALLER supplied the duplicate: the column may be
		// filled in by the server (hr allocates employee_number, and
		// employees_employee_number_key collided in production for exactly
		// that reason). Telling the caller their value is taken when they sent
		// no value sends them looking for a duplicate they never entered.
		//
		// "already exists" states the fact that is true in both cases without
		// asserting who supplied it. A caller who DID supply the value can still
		// act on it - a NIK, a code, an email all read as "change it" - and a
		// module that can distinguish the two maps its own case to a precise
		// message before reaching here (see hr.mapEmployeeInsertErr).
		return "A record with this value already exists.", true
	case "23502": // not_null_violation
		return "A required value is missing.", true
	case "23503": // foreign_key_violation
		return "A referenced record does not exist.", true
	case "23514": // check_violation
		return "One of the submitted values is not allowed.", true
	case "22001": // string_data_right_truncation
		return "One of the submitted values is too long.", true
	case "22003": // numeric_value_out_of_range
		return "One of the submitted numbers is out of range.", true
	case "22007", "22008": // invalid_datetime_format, datetime_field_overflow
		return "One of the submitted dates is not a valid date.", true
	case "22P02": // invalid_text_representation
		return "One of the submitted values has the wrong format.", true
	}
	return "", false
}

// logDatabaseError records what was withheld from the response.
//
// A data violation's driver text quotes the rejected row — DETAIL: Key
// (email)=(...) already exists — so for that class only the identity of the
// rule is logged; constraint name and SQLSTATE are what an operator needs to
// find the rule in the schema, and the rejected values are the caller's data
// and stay out of the log. For a fault the caller did not cause there are no
// rejected values to protect, so the whole error is the useful diagnostic.
func logDatabaseError(log ErrorLogger, requestID string, err error) {
	if log == nil {
		return
	}
	var pge *pgconn.PgError
	if errors.As(err, &pge) {
		if _, isData := dataViolationMessage(pge.Code); isData {
			log.Error("database_error",
				"request_id", requestID,
				"sqlstate", pge.Code,
				"constraint", pge.ConstraintName,
				"table", pge.TableName,
				"column", pge.ColumnName)
			return
		}
	}
	log.Error("database_error", "request_id", requestID, "err", err)
}

// SafeMessage returns text that is safe to send to the client for err.
//
// Two classes are rewritten, both because their text is written for a server
// operator rather than for the caller:
//
//   - a database driver error, replaced by the message for its SQLSTATE class;
//   - a JSON body that would not decode, whose text names a Go type and a
//     package for a request that is merely malformed.
//
// Every other error is returned unchanged, so the sentinel errors a handler
// deliberately maps keep the wording their author chose for them.
func SafeMessage(err error) string {
	if err == nil {
		return ""
	}
	if _, _, message, ok := dbSafeResponse(err); ok {
		return message
	}
	if isDecodeError(err) {
		return badBodyMessage(err)
	}
	return err.Error()
}

// isDecodeError reports whether err is the decoder reporting a body it could
// not read. io.EOF is deliberately not included: a decoder returns it for an
// empty body, but io.EOF reaching an error writer from anywhere else means the
// stream ended, and calling that "Request body is required." would be a lie.
func isDecodeError(err error) bool {
	var syntaxErr *json.SyntaxError
	var typeErr *json.UnmarshalTypeError
	return errors.As(err, &syntaxErr) || errors.As(err, &typeErr)
}

// SafeError answers err for a handler that reached it without a matching
// sentinel, keeping the status and code the handler chose for every error that
// is not a database driver error. A driver error is classified instead: see
// dbSafeResponse for why a data violation is a 400 and a fault is a 500.
func SafeError(w http.ResponseWriter, requestID string, log ErrorLogger, status int, code ErrorCode, err error) {
	if err == nil {
		return
	}
	safeStatus, safeCode, message, ok := dbSafeResponse(err)
	if !ok {
		Error(w, status, requestID, code, err.Error(), nil)
		return
	}
	logDatabaseError(log, requestID, err)
	Error(w, safeStatus, requestID, safeCode, message, nil)
}

// DecodeJSON decodes the request body into dst. It answers 400 itself and
// returns false when the body would not decode, so a caller that gets false
// has already written the response and must return.
//
// requestID is passed in rather than read from the request context because
// pkg/middleware depends on this package.
func DecodeJSON(w http.ResponseWriter, r *http.Request, requestID string, dst any) bool {
	if r.Body == nil {
		Error(w, http.StatusBadRequest, requestID, CodeValidation, "Request body is required.", nil)
		return false
	}
	defer r.Body.Close()
	if err := json.NewDecoder(r.Body).Decode(dst); err != nil {
		Error(w, http.StatusBadRequest, requestID, CodeValidation, badBodyMessage(err), nil)
		return false
	}
	return true
}

// badBodyMessage describes a body that would not decode, in the caller's terms.
// Only the request's own field names are quoted back: those are in the
// caller's request and in the API contract already, whereas the Go type name
// the decoder also reports is not.
func badBodyMessage(err error) string {
	var syntaxErr *json.SyntaxError
	var typeErr *json.UnmarshalTypeError
	switch {
	case errors.Is(err, io.EOF):
		return "Request body is required."
	case errors.As(err, &syntaxErr):
		return "Request body is not valid JSON."
	case errors.As(err, &typeErr) && typeErr.Field != "":
		return "Field \"" + typeErr.Field + "\" has the wrong type."
	}
	return "Request body is not in the expected format."
}
