#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <net/if.h>
#include <netinet/in.h>
#include <ifaddrs.h>
#include <errno.h>

#include <json-c/json.h>

#ifdef HAVE_SYS_UTSNAME_H
# include <sys/utsname.h>
#endif

#include "librespot-c-internal.h"
#include "connection.h"
#include "channel.h"
#include "http.h"

#define MERCURY_REQ_SIZE_MAX 4096

// Forgot how I arrived at this upper bound
#define HASHCASH_ITERATIONS_MAX 100000

static struct timeval sp_idle_tv = { SP_AP_DISCONNECT_SECS, 0 };

static uint8_t sp_aes_iv[] = { 0x72, 0xe0, 0x67, 0xfb, 0xdd, 0xcb, 0xcf, 0x77, 0xeb, 0xe8, 0xbc, 0x64, 0x3f, 0x63, 0x0d, 0x93 };

static struct sp_err_map sp_login_errors[] = {
  { ERROR_CODE__ProtocolError, "Protocol error" },
  { ERROR_CODE__TryAnotherAP, "Try another access point" },
  { ERROR_CODE__BadConnectionId, "Bad connection ID" },
  { ERROR_CODE__TravelRestriction, "Travel restriction" },
  { ERROR_CODE__PremiumAccountRequired, "Premium account required" },
  { ERROR_CODE__BadCredentials, "Bad credentials" },
  { ERROR_CODE__CouldNotValidateCredentials, "Could not validate credentials" },
  { ERROR_CODE__AccountExists, "Account exists" },
  { ERROR_CODE__ExtraVerificationRequired, "Extra verification required" },
  { ERROR_CODE__InvalidAppKey, "Invalid app key" },
  { ERROR_CODE__ApplicationBanned, "Application banned" },
};

static struct sp_err_map sp_login5_warning_map[] = {
  { SPOTIFY__LOGIN5__V3__LOGIN_RESPONSE__WARNINGS__UNKNOWN_WARNING, "Unknown warning" },
  { SPOTIFY__LOGIN5__V3__LOGIN_RESPONSE__WARNINGS__DEPRECATED_PROTOCOL_VERSION, "Deprecated protocol" },
};

static struct sp_err_map sp_login5_error_map[] = {
  { SPOTIFY__LOGIN5__V3__LOGIN_ERROR__UNKNOWN_ERROR, "Unknown error" },
  { SPOTIFY__LOGIN5__V3__LOGIN_ERROR__INVALID_CREDENTIALS, "Invalid credentials" },
  { SPOTIFY__LOGIN5__V3__LOGIN_ERROR__BAD_REQUEST, "Bad request" },
  { SPOTIFY__LOGIN5__V3__LOGIN_ERROR__UNSUPPORTED_LOGIN_PROTOCOL, "Unsupported login protocol" },
  { SPOTIFY__LOGIN5__V3__LOGIN_ERROR__TIMEOUT, "Timeout" },
  { SPOTIFY__LOGIN5__V3__LOGIN_ERROR__UNKNOWN_IDENTIFIER, "Unknown identifier" },
  { SPOTIFY__LOGIN5__V3__LOGIN_ERROR__TOO_MANY_ATTEMPTS, "Too many attempts" },
  { SPOTIFY__LOGIN5__V3__LOGIN_ERROR__INVALID_PHONENUMBER, "Invalid phonenumber" },
  { SPOTIFY__LOGIN5__V3__LOGIN_ERROR__TRY_AGAIN_LATER, "Try again later" },
};

/* ---------------------------------- MOCKING ------------------------------- */

#ifdef DEBUG_MOCK
#include "mock.h"

static int debug_mock_pipe[2];
static int debug_mock_chunks_written = 0;

static int
debug_mock_response(struct sp_message *msg, struct sp_connection *conn)
{
  if (debug_mock_chunks_written == 1)
    return 0; // Only write the chunk once

  switch(msg->type)
    {
      case MSG_TYPE_CLIENT_HELLO:
	write(debug_mock_pipe[1], mock_resp_apresponse, sizeof(mock_resp_apresponse));
	break;
      case MSG_TYPE_CLIENT_RESPONSE_ENCRYPTED:
        memcpy(conn->decrypt.key, mock_recv_key, sizeof(conn->decrypt.key));
	// Spotify will send the replies split in these 6 packets
	write(debug_mock_pipe[1], mock_resp_client_encrypted1, sizeof(mock_resp_client_encrypted1));
	write(debug_mock_pipe[1], mock_resp_client_encrypted2, sizeof(mock_resp_client_encrypted2));
	write(debug_mock_pipe[1], mock_resp_client_encrypted3, sizeof(mock_resp_client_encrypted3));
	write(debug_mock_pipe[1], mock_resp_client_encrypted4, sizeof(mock_resp_client_encrypted4));
	write(debug_mock_pipe[1], mock_resp_client_encrypted5, sizeof(mock_resp_client_encrypted5));
	write(debug_mock_pipe[1], mock_resp_client_encrypted6, sizeof(mock_resp_client_encrypted6));
	break;
      case MSG_TYPE_MERCURY_TRACK_GET:
	write(debug_mock_pipe[1], mock_resp_mercury_req1, sizeof(mock_resp_mercury_req1));
	write(debug_mock_pipe[1], mock_resp_mercury_req2, sizeof(mock_resp_mercury_req2));
	break;
      case MSG_TYPE_AUDIO_KEY_GET:
        memset(conn->decrypt.key, 0, sizeof(conn->decrypt.key)); // Tells msg_read_one() to skip decryption
	write(debug_mock_pipe[1], mock_resp_aeskey, sizeof(mock_resp_aeskey));
	break;
      case MSG_TYPE_CHUNK_REQUEST:
	write(debug_mock_pipe[1], mock_resp_chunk1, sizeof(mock_resp_chunk1));
	write(debug_mock_pipe[1], mock_resp_chunk2, sizeof(mock_resp_chunk2));
	debug_mock_chunks_written++;
	break;
      default:
	break;
    }

  return 0;
}
#endif


/* --------------------------------- Helpers -------------------------------- */

static char *
asprintf_or_die(const char *fmt, ...)
{
  char *ret = NULL;
  va_list va;

  va_start(va, fmt);
  if (vasprintf(&ret, fmt, va) < 0)
    {
      sp_cb.logmsg("Out of memory for asprintf\n");
      abort();
    }
  va_end(va);

  return ret;
}

#ifdef HAVE_SYS_UTSNAME_H
static void
system_info_from_uname(SystemInfo *system_info)
{
  struct utsname uts = { 0 };

  if (uname(&uts) < 0)
    return;

  if (strcmp(uts.sysname, "Linux") == 0)
    system_info->os = OS__OS_LINUX;
  else if (strcmp(uts.sysname, "Darwin") == 0)
    system_info->os = OS__OS_OSX;
  else if (strcmp(uts.sysname, "FreeBSD") == 0)
    system_info->os = OS__OS_FREEBSD;

  if (strcmp(uts.machine, "x86_64") == 0)
    system_info->cpu_family = CPU_FAMILY__CPU_X86_64;
  else if (strncmp(uts.machine, "arm", 3) == 0)
    system_info->cpu_family = CPU_FAMILY__CPU_ARM;
  else if (strcmp(uts.machine, "aarch64") == 0)
    system_info->cpu_family = CPU_FAMILY__CPU_ARM;
  else if (strcmp(uts.machine, "i386") == 0)
    system_info->cpu_family = CPU_FAMILY__CPU_X86;
  else if (strcmp(uts.machine, "i686") == 0)
    system_info->cpu_family = CPU_FAMILY__CPU_X86;
  else if (strcmp(uts.machine, "ppc") == 0)
    system_info->cpu_family = CPU_FAMILY__CPU_PPC;
  else if (strcmp(uts.machine, "ppc64") == 0)
    system_info->cpu_family = CPU_FAMILY__CPU_PPC_64;
}
#else
static void
system_info_from_uname(SystemInfo *system_info)
{
  return;
}
#endif

// Returns true if format of a is preferred over b (and is valid). According to
// librespot comment most podcasts are 96 kbit.
static bool
format_is_preferred(AudioFile *a, AudioFile *b, enum sp_bitrates bitrate_preferred)
{
  if (a->format != AUDIO_FILE__FORMAT__OGG_VORBIS_96 &&
      a->format != AUDIO_FILE__FORMAT__OGG_VORBIS_160 &&
      a->format != AUDIO_FILE__FORMAT__OGG_VORBIS_320)
    return false;

  if (!b)
    return true; // Any format is better than no format

  switch (bitrate_preferred)
    {
      case SP_BITRATE_96:
	return (a->format < b->format); // Prefer lowest
      case SP_BITRATE_160:
	if (b->format == AUDIO_FILE__FORMAT__OGG_VORBIS_160)
	  return false;
	else if (a->format == AUDIO_FILE__FORMAT__OGG_VORBIS_160)
	  return true;
	else
	  return (a->format < b->format); // Prefer lowest
      case SP_BITRATE_320:
	return (a->format > b->format); // Prefer highest
      case SP_BITRATE_ANY:
	return (a->format > b->format); // This case shouldn't happen, so this is mostly to avoid compiler warnings
    }

  return false;
}

int
file_select(uint8_t *out, size_t out_len, Track *track, enum sp_bitrates bitrate_preferred)
{
  AudioFile *selected = NULL;
  AudioFile *file;
  int i;

  for (i = 0; i < track->n_file; i++)
    {
      file = track->file[i];

      if (!file->has_file_id || !file->has_format || file->file_id.len != out_len)
	continue;

      if (format_is_preferred(file, selected, bitrate_preferred))
	selected = file;
    }

  if (!selected)
    return -1;

  memcpy(out, selected->file_id.data, selected->file_id.len);

  return 0;
}

static const char *
err2txt(int err, struct sp_err_map *map, size_t map_size)
{
  for (int i = 0; i < map_size; i++)
    {
      if (err == map[i].errorcode)
        return map[i].errmsg;
    }

  return "(unknown error code)";
}

/* --------------------------- Connection handling -------------------------- */

static void
tcp_connection_clear(struct sp_connection *conn)
{
  if (!conn)
    return;

  if (conn->response_ev)
    event_free(conn->response_ev);
  if (conn->idle_ev)
    event_free(conn->idle_ev);
  if (conn->timeout_ev)
    event_free(conn->timeout_ev);

  if (conn->handshake_packets)
    evbuffer_free(conn->handshake_packets);
  if (conn->incoming)
    evbuffer_free(conn->incoming);

  free(conn->keys.shared_secret);

  memset(conn, 0, sizeof(struct sp_connection));
  conn->response_fd = -1;
}

static void
tcp_connection_idle_cb(int fd, short what, void *arg)
{
  struct sp_connection *conn = arg;

  ap_disconnect(conn);

  sp_cb.logmsg("Connection is idle, auto-disconnected\n");
}

static int
tcp_connection_make(struct sp_connection *conn, struct sp_server *server, struct sp_conn_callbacks *cb, void *cb_arg)
{
  int response_fd;
  int ret;

#ifndef DEBUG_MOCK
  response_fd = sp_cb.tcp_connect(server->address, server->port);
  if (response_fd < 0)
    RETURN_ERROR(SP_ERR_NOCONNECTION, "Could not connect to access point");
#else
  pipe(debug_mock_pipe);
  response_fd = debug_mock_pipe[0];
#endif

  server->last_connect_ts = time(NULL);
  conn->server = server;

  conn->response_fd = response_fd;
  conn->response_ev = event_new(cb->evbase, response_fd, EV_READ | EV_PERSIST, cb->response_cb, cb_arg);
  conn->timeout_ev = evtimer_new(cb->evbase, cb->timeout_cb, cb_arg);

  conn->idle_ev = evtimer_new(cb->evbase, tcp_connection_idle_cb, conn);

  conn->handshake_packets = evbuffer_new();
  conn->incoming = evbuffer_new();

  crypto_keys_set(&conn->keys);
  conn->encrypt.logmsg = sp_cb.logmsg;
  conn->decrypt.logmsg = sp_cb.logmsg;

  event_add(conn->response_ev, NULL);

  conn->is_connected = true;

  return 0;

 error:
  server->last_failed_ts = time(NULL);
  return ret;
}

static int
must_resolve(struct sp_server *server)
{
  time_t now = time(NULL);

  return (server->last_resolved_ts == 0) || (server->last_failed_ts + SP_AP_AVOID_SECS > now);
}

void
ap_disconnect(struct sp_connection *conn)
{
  if (conn->is_connected)
    sp_cb.tcp_disconnect(conn->response_fd);

  tcp_connection_clear(conn);
}

enum sp_error
ap_connect(struct sp_connection *conn, struct sp_server *server, time_t *cooldown_ts, struct sp_conn_callbacks *cb, void *cb_arg)
{
  int ret;
  time_t now;

  // Protection against flooding the access points with reconnection attempts
  // Note that cooldown_ts can't be part of the connection struct because
  // the struct is reset between connection attempts.
  now = time(NULL);
  if (now > *cooldown_ts + SP_AP_COOLDOWN_SECS) // Last attempt was a long time ago
    *cooldown_ts = now;
  else if (now >= *cooldown_ts) // Last attempt was recent, so disallow more attempts for a while
    *cooldown_ts = now + SP_AP_COOLDOWN_SECS;
  else
    RETURN_ERROR(SP_ERR_NOCONNECTION, "Cannot connect to access points, cooldown after multiple disconnects");

  // This server has recently failed, so tell caller to try another
  if (must_resolve(server))
    {
      sp_cb.logmsg("Server '%s' no longer valid\n", server->address);
      goto retry;
    }

  if (conn->is_connected)
    ap_disconnect(conn);

  ret = tcp_connection_make(conn, server, cb, cb_arg);
  if (ret < 0)
    {
      sp_cb.logmsg("Couldn't connect to '%s': %s\n", server->address, sp_errmsg);
      goto retry;
    }

  return SP_OK_DONE;

 error:
  ap_disconnect(conn);
  return ret;

 retry:
  return SP_OK_WAIT; // Tells caller to try another
}

void
ap_blacklist(struct sp_server *server)
{
  server->last_failed_ts = time(NULL);
}

/* ------------------------------ Raw packets ------------------------------- */

static ssize_t
packet_make_encrypted(uint8_t *out, size_t out_len, uint8_t cmd, const uint8_t *payload, size_t payload_len, struct crypto_cipher *cipher)
{
  uint16_t be;
  size_t plain_len;
  ssize_t pkt_len;
  uint8_t *ptr;

  be = htobe16(payload_len);

  plain_len = sizeof(cmd) + sizeof(be) + payload_len;
  if (plain_len > out_len)
    goto error;

  ptr = out;
  memcpy(ptr, &cmd, sizeof(cmd));
  ptr += sizeof(cmd);
  memcpy(ptr, &be, sizeof(be));
  ptr += sizeof(be);
  memcpy(ptr, payload, payload_len);

//  sp_cb.hexdump("Encrypting packet\n", out, plain_len);

  pkt_len = crypto_encrypt(out, out_len, plain_len, cipher);
  if (pkt_len < 9)
    goto error;

  return pkt_len;

 error:
  return -1;
}

static ssize_t
packet_make_plain(uint8_t *out, size_t out_len, uint8_t *protobuf, size_t protobuf_len, bool add_version_header)
{
  const uint8_t version_header[] = { 0x00, 0x04 };
  size_t header_len;
  ssize_t len;
  uint32_t be;

  header_len = add_version_header ? sizeof(be) + sizeof(version_header) : sizeof(be);

  len = header_len + protobuf_len;
  if (len > out_len)
    return -1;

  if (add_version_header)
    memcpy(out, version_header, sizeof(version_header));

  be = htobe32(len);
  memcpy(out + header_len - sizeof(be), &be, sizeof(be)); // Last bytes of the header is the length
  memcpy(out + header_len, protobuf, protobuf_len);

  return len;
}


/* ---------------------------- Mercury messages ---------------------------- */

static void
mercury_free(struct sp_mercury *mercury, int content_only)
{
  int i;

  if (!mercury)
    return;

  free(mercury->uri);
  free(mercury->method);
  free(mercury->content_type);

  for (i = 0; i < mercury->parts_num; i++)
    {
      free(mercury->parts[i].data);

      if (mercury->parts[i].track)
	track__free_unpacked(mercury->parts[i].track, NULL);
    }

  if (content_only)
    memset(mercury, 0, sizeof(struct sp_mercury));
  else
    free(mercury);
}

static int
mercury_parse(struct sp_mercury *mercury, uint8_t *payload, size_t payload_len)
{
  Header *header;
  uint8_t *ptr;
  uint16_t be;
  uint64_t be64;
  uint16_t seq_len;
  uint16_t header_len;
  size_t required_len; // For size checking
  uint8_t flags;
  int i;

  ptr = payload;

  required_len = sizeof(be);
  if (required_len > payload_len)
    goto error_length; // Length check 1

  memcpy(&be, ptr, sizeof(be));
  seq_len = be16toh(be);
  ptr += sizeof(be); // 1: length += sizeof(be)

  required_len += seq_len + sizeof(flags) + sizeof(be) + sizeof(be);
  if (required_len > payload_len || seq_len != sizeof(be64))
    goto error_length; // Length check 2

  memcpy(&be64, ptr, sizeof(be64));
  mercury->seq = be64toh(be64);
  ptr += seq_len; // 2: length += seq_len

  memcpy(&flags, ptr, sizeof(flags));
  ptr += sizeof(flags); // 2: length += sizeof(flags)

  memcpy(&be, ptr, sizeof(be));
  mercury->parts_num = be16toh(be) - 1; // What's the deal with the 1...?
  ptr += sizeof(be); // 2: length += sizeof(be)

  if (mercury->parts_num > SP_MERCURY_MAX_PARTS)
    return -1;

  memcpy(&be, ptr, sizeof(be));
  header_len = be16toh(be);
  ptr += sizeof(be); // 2: length += sizeof(be)

  required_len += header_len;
  if (required_len > payload_len)
    goto error_length; // Length check 3

  header = header__unpack(NULL, header_len, ptr);
  if (!header)
    goto error_length;
  ptr += header_len; // 3: length += header_len

  mercury->uri = header->uri ? strdup(header->uri) : NULL;
  mercury->method = header->method ? strdup(header->method) : NULL;
  mercury->content_type = header->content_type ? strdup(header->content_type) : NULL;

  for (i = 0; i < mercury->parts_num; i++)
    {
      required_len += sizeof(be);
      if (required_len > payload_len)
	goto error_length; // Length check 4

      memcpy(&be, ptr, sizeof(be));
      mercury->parts[i].len = be16toh(be);
      ptr += sizeof(be); // 4: length += sizeof(be)

      required_len += mercury->parts[i].len;
      if (required_len > payload_len)
	goto error_length; // Length check 5

      mercury->parts[i].data = malloc(mercury->parts[i].len);
      memcpy(mercury->parts[i].data, ptr, mercury->parts[i].len);
      ptr += mercury->parts[i].len; // 5: length += mercury->parts[i].len

      mercury->parts[i].track = track__unpack(NULL, mercury->parts[i].len, mercury->parts[i].data);
    }

  header__free_unpacked(header, NULL);

  assert(ptr == payload + required_len);

  return 0;

 error_length:
  mercury_free(mercury, 1);
  return -1;
}


/* ---------------------Request preparation (dependencies) ------------------ */

static enum sp_error
prepare_tcp_handshake(struct sp_seq_request *request, struct sp_conn_callbacks *cb, struct sp_session *session)
{
  int ret;

  if (!session->conn.is_connected)
    {
      ret = ap_connect(&session->conn, &session->accesspoint, &session->cooldown_ts, cb, session);
      if (ret == SP_OK_WAIT) // Try another server
	{
	  if (request->seq_type != SP_SEQ_LOGIN)
	    seq_next_set(session, request->seq_type);

	  session->request = seq_request_get(SP_SEQ_LOGIN, 0, session->use_legacy);
	  return SP_OK_WAIT;
	}
      else if (ret < 0)
	RETURN_ERROR(ret, sp_errmsg);
    }

  return SP_OK_DONE;

 error:
  return ret;
}

static enum sp_error
prepare_tcp(struct sp_seq_request *request, struct sp_conn_callbacks *cb, struct sp_session *session)
{
  int ret;

  ret = prepare_tcp_handshake(request, cb, session);
  if (ret != SP_OK_DONE)
    return ret; // SP_OK_WAIT if the current AP failed and we need to try a new one

  if (!session->conn.handshake_completed)
    {
      // Queue the current request
      seq_next_set(session, request->seq_type);
      session->request = seq_request_get(SP_SEQ_LOGIN, 0, session->use_legacy);
      return SP_OK_WAIT;
    }

  return SP_OK_DONE;
}


/* --------------------------- Incoming messages ---------------------------- */

static enum sp_error
resolve_server_info_set(struct sp_server *server, const char *key, json_object *jresponse)
{
  json_object *list;
  json_object *instance;
  size_t address_len;
  const char *s;
  char *colon;
  bool is_same;
  bool has_failed;
  int ret;
  int n;
  int i;

  has_failed = (server->last_failed_ts + SP_AP_AVOID_SECS > time(NULL));

  if (! (json_object_object_get_ex(jresponse, key, &list) || json_object_get_type(list) == json_type_array))
    RETURN_ERROR(SP_ERR_NOCONNECTION, "No address list in response from access point resolver");

  n = json_object_array_length(list);
  for (i = 0, s = NULL; i < n && !s; i++)
    {
      instance = json_object_array_get_idx(list, i);
      if (! (instance && json_object_get_type(instance) == json_type_string))
        RETURN_ERROR(SP_ERR_NOCONNECTION, "Unexpected data in response from access point resolver");

      s = json_object_get_string(instance); // This string includes the port
      address_len = strlen(server->address);
      is_same = (address_len > 0) && (strncmp(s, server->address, address_len) == 0);

      if (is_same && has_failed)
        s = NULL; // This AP has failed on us recently, so avoid
    }

  if (!s)
    RETURN_ERROR(SP_ERR_NOCONNECTION, "Response from resolver had no valid servers");

  if (!is_same)
    {
      memset(server, 0, sizeof(struct sp_server));
      ret = snprintf(server->address, sizeof(server->address), "%s", s);
      if (ret < 0 || ret >= sizeof(server->address))
	RETURN_ERROR(SP_ERR_INVALID, "AP resolver returned an address that is too long");

      colon = strchr(server->address, ':');
      if (colon)
        *colon = '\0';

      server->port = colon ? (unsigned short)atoi(colon + 1) : 443;
    }

  server->last_resolved_ts = time(NULL);
  return SP_OK_DONE;

 error:
  return ret;
}

static enum sp_error
handle_ap_resolve(struct sp_message *msg, struct sp_session *session)
{
  struct http_response *hres = &msg->payload.hres;
  json_object *jresponse = NULL;
  int ret;

  if (hres->code != HTTP_OK)
    RETURN_ERROR(SP_ERR_NOCONNECTION, "AP resolver returned an error");

  jresponse = json_tokener_parse((char *)hres->body);
  if (!jresponse)
    RETURN_ERROR(SP_ERR_NOCONNECTION, "Could not parse reply from access point resolver");

  ret = resolve_server_info_set(&session->accesspoint, "accesspoint", jresponse);
  if (ret < 0)
    goto error;

  ret = resolve_server_info_set(&session->spclient, "spclient", jresponse);
  if (ret < 0)
    goto error;

  ret = resolve_server_info_set(&session->dealer, "dealer", jresponse);
  if (ret < 0)
    goto error;

  json_object_put(jresponse);
  return SP_OK_DONE;

 error:
  json_object_put(jresponse);
  return ret;
}

static enum sp_error
handle_client_hello(struct sp_message *msg, struct sp_session *session)
{
  uint8_t *payload = msg->payload.tmsg.data;
  size_t payload_len = msg->payload.tmsg.len;
  APResponseMessage *apresponse;
  struct sp_connection *conn = &session->conn;
  int ret;

  // The first 4 bytes should be the size of the message
  if (payload_len < 4)
    RETURN_ERROR(SP_ERR_INVALID, "Invalid apresponse from access point");

  apresponse = apresponse_message__unpack(NULL, payload_len - 4, payload + 4);
  if (!apresponse)
    RETURN_ERROR(SP_ERR_INVALID, "Could not unpack apresponse from access point");

  // TODO check APLoginFailed

  // Not sure if necessary
  if (!apresponse->challenge || !apresponse->challenge->login_crypto_challenge)
    RETURN_ERROR(SP_ERR_INVALID, "Missing challenge in response from access point");

  crypto_shared_secret(
    &conn->keys.shared_secret, &conn->keys.shared_secret_len,
    conn->keys.private_key, sizeof(conn->keys.private_key),
    apresponse->challenge->login_crypto_challenge->diffie_hellman->gs.data, apresponse->challenge->login_crypto_challenge->diffie_hellman->gs.len);

  apresponse_message__free_unpacked(apresponse, NULL);

  conn->handshake_completed = true;

  return SP_OK_DONE;

 error:
  return ret;
}

static enum sp_error
handle_apwelcome(uint8_t *payload, size_t payload_len, struct sp_session *session)
{
  APWelcome *apwelcome;
  int ret;

  apwelcome = apwelcome__unpack(NULL, payload_len, payload);
  if (!apwelcome)
    RETURN_ERROR(SP_ERR_INVALID, "Could not unpack apwelcome response from access point");

  if (apwelcome->reusable_auth_credentials_type == AUTHENTICATION_TYPE__AUTHENTICATION_STORED_SPOTIFY_CREDENTIALS)
    {
      if (apwelcome->reusable_auth_credentials.len > sizeof(session->credentials.stored_cred))
	RETURN_ERROR(SP_ERR_INVALID, "Credentials from Spotify longer than expected");

      session->credentials.stored_cred_len = apwelcome->reusable_auth_credentials.len;
      memcpy(session->credentials.stored_cred, apwelcome->reusable_auth_credentials.data, session->credentials.stored_cred_len);

      // No need for this any more
      memset(session->credentials.password, 0, sizeof(session->credentials.password));
    }

  apwelcome__free_unpacked(apwelcome, NULL);

  return SP_OK_DONE;

 error:
  return ret;
}

static enum sp_error
handle_aplogin_failed(uint8_t *payload, size_t payload_len, struct sp_session *session)
{
  APLoginFailed *aplogin_failed;

  aplogin_failed = aplogin_failed__unpack(NULL, payload_len, payload);
  if (!aplogin_failed)
    {
      sp_errmsg = "Could not unpack login failure from access point";
      return SP_ERR_LOGINFAILED;
    }

  sp_errmsg = err2txt(aplogin_failed->error_code, sp_login_errors, ARRAY_SIZE(sp_login_errors));

  aplogin_failed__free_unpacked(aplogin_failed, NULL);

  return SP_ERR_LOGINFAILED;
}

static enum sp_error
handle_chunk_res(uint8_t *payload, size_t payload_len, struct sp_session *session)
{
  struct sp_channel *channel;
  uint16_t channel_id;
  int ret;

  ret = channel_msg_read(&channel_id, payload, payload_len, session);
  if (ret < 0)
    goto error;

  channel = &session->channels[channel_id];

  // Save any audio data to a buffer that will be written to audio_fd[1] when
  // it is writable. Note that request for next chunk will also happen then.
  evbuffer_add(channel->audio_buf, channel->body.data, channel->body.data_len);

//  sp_cb.logmsg("EOC is %d, data is %zu, buflen is %zu\n", channel->file.end_of_chunk, channel->body.data_len, evbuffer_get_length(channel->audio_buf));

  return channel->file.end_of_chunk ? SP_OK_DATA : SP_OK_OTHER;

 error:
  return ret;
}

static enum sp_error
handle_aes_key(uint8_t *payload, size_t payload_len, struct sp_session *session)
{
  struct sp_channel *channel;
  const char *errmsg;
  uint32_t be32;
  uint32_t channel_id;
  int ret;

  // Payload is expected to consist of seq (uint32 BE), and key (16 bytes)
  if (payload_len != sizeof(be32) + 16)
    RETURN_ERROR(SP_ERR_DECRYPTION, "Unexpected key received");

  memcpy(&be32, payload, sizeof(be32));
  channel_id = be32toh(be32);

  channel = channel_get(channel_id, session);
  if (!channel)
    RETURN_ERROR(SP_ERR_INVALID, "Unexpected channel received");

  memcpy(channel->file.key, payload + 4, 16);

  ret = crypto_aes_new(&channel->file.decrypt, channel->file.key, sizeof(channel->file.key), sp_aes_iv, sizeof(sp_aes_iv), &errmsg);
  if (ret < 0)
    RETURN_ERROR(SP_ERR_DECRYPTION, errmsg);

  return SP_OK_DONE;

 error:
  return ret;
}

static enum sp_error
handle_aes_key_error(uint8_t *payload, size_t payload_len, struct sp_session *session)
{
  sp_errmsg = "Did not get key for decrypting track";

  return SP_ERR_DECRYPTION;
}

// AP in bad state may return a channel error after chunk request. In that case
// we error with NOCONNECTION, because that will make the main session handler
// (see response_cb) retry with another access point. An example of this issue
// is here https://github.com/librespot-org/librespot/issues/972
static enum sp_error
handle_channel_error(uint8_t *payload, size_t payload_len, struct sp_session *session)
{
  sp_errmsg = "The accces point returned a channel error";

  return SP_ERR_NOCONNECTION;
}

static enum sp_error
handle_mercury_req(uint8_t *payload, size_t payload_len, struct sp_session *session)
{
  struct sp_mercury mercury = { 0 };
  struct sp_channel *channel;
  uint32_t channel_id;
  int ret;

  ret = mercury_parse(&mercury, payload, payload_len);
  if (ret < 0)
    {
      sp_errmsg = "Could not parse message from Spotify";
      return SP_ERR_INVALID;
    }

  if (mercury.parts_num != 1 || !mercury.parts[0].track)
    RETURN_ERROR(SP_ERR_INVALID, "Unexpected track response from Spotify");

  channel_id = (uint32_t)mercury.seq;

  channel = channel_get(channel_id, session);
  if (!channel)
    RETURN_ERROR(SP_ERR_INVALID, "Unexpected channel received");

  ret = file_select(channel->file.id, sizeof(channel->file.id), mercury.parts[0].track, session->bitrate_preferred);
  if (ret < 0)
    RETURN_ERROR(SP_ERR_INVALID, "Could not find track data");

  mercury_free(&mercury, 1);

  return SP_OK_DONE; // Continue to get AES key

 error:
  mercury_free(&mercury, 1);
  return ret;
}

static enum sp_error
handle_ping(uint8_t *payload, size_t payload_len, struct sp_session *session)
{
  msg_pong(session);

  return SP_OK_OTHER;
}

static enum sp_error
handle_clienttoken(struct sp_message *msg, struct sp_session *session)
{
  struct http_response *hres = &msg->payload.hres;
  struct sp_token *token = &session->http_clienttoken;
  Spotify__Clienttoken__Http__V0__ClientTokenResponse *response = NULL;
  int ret;

  if (hres->code != HTTP_OK)
    RETURN_ERROR(SP_ERR_INVALID, "Request to clienttoken returned an error");

  response = spotify__clienttoken__http__v0__client_token_response__unpack(NULL, hres->body_len, hres->body);
  if (!response)
    RETURN_ERROR(SP_ERR_INVALID, "Could not parse clienttoken response");

  if (response->response_type == SPOTIFY__CLIENTTOKEN__HTTP__V0__CLIENT_TOKEN_RESPONSE_TYPE__RESPONSE_GRANTED_TOKEN_RESPONSE)
    {
      ret = snprintf(token->value, sizeof(token->value), "%s", response->granted_token->token);
      if (ret < 0 || ret >= sizeof(token->value))
	RETURN_ERROR(SP_ERR_INVALID, "Unexpected clienttoken length");

      token->expires_after_seconds = response->granted_token->expires_after_seconds;
      token->refresh_after_seconds = response->granted_token->refresh_after_seconds;
      token->received_ts = time(NULL);
    }
  else if (response->response_type == SPOTIFY__CLIENTTOKEN__HTTP__V0__CLIENT_TOKEN_RESPONSE_TYPE__RESPONSE_CHALLENGES_RESPONSE)
    RETURN_ERROR(SP_ERR_INVALID, "Unsupported clienttoken response");
  else
    RETURN_ERROR(SP_ERR_INVALID, "Unknown clienttoken response");

  spotify__clienttoken__http__v0__client_token_response__free_unpacked(response, NULL);
  return SP_OK_DONE;

 error:
  spotify__clienttoken__http__v0__client_token_response__free_unpacked(response, NULL);
  return ret;
}

static void
hashcash_challenges_free(struct crypto_hashcash_challenge **challenges, int *n_challenges)
{
  for (int i = 0; i < *n_challenges; i++)
    free(challenges[i]->ctx);

  free(*challenges);
  *challenges = NULL;
  *n_challenges = 0;
}

static enum sp_error
handle_login5_challenges(Spotify__Login5__V3__Challenges *challenges, uint8_t *login_ctx, size_t login_ctx_len, struct sp_session *session)
{
  Spotify__Login5__V3__Challenge *this_challenge;
  struct crypto_hashcash_challenge *crypto_challenge;
  int ret;
  int i;

  session->n_hashcash_challenges = challenges->n_challenges;
  session->hashcash_challenges = calloc(challenges->n_challenges, sizeof(struct crypto_hashcash_challenge));

  for (i = 0, crypto_challenge = session->hashcash_challenges; i < session->n_hashcash_challenges; i++, crypto_challenge++)
    {
      this_challenge = challenges->challenges[i];

      if (this_challenge->challenge_case != SPOTIFY__LOGIN5__V3__CHALLENGE__CHALLENGE_HASHCASH)
	RETURN_ERROR(SP_ERR_INVALID, "Received unsupported login5 challenge");

      if (this_challenge->hashcash->prefix.len != sizeof(crypto_challenge->prefix))
	RETURN_ERROR(SP_ERR_INVALID, "Received hashcash challenge with unexpected prefix length");

      crypto_challenge->ctx_len = login_ctx_len;
      crypto_challenge->ctx = malloc(login_ctx_len);
      memcpy(crypto_challenge->ctx, login_ctx, login_ctx_len);

      memcpy(crypto_challenge->prefix, this_challenge->hashcash->prefix.data, sizeof(crypto_challenge->prefix));
      crypto_challenge->wanted_zero_bits = this_challenge->hashcash->length;
      crypto_challenge->max_iterations = HASHCASH_ITERATIONS_MAX;

    }

  return SP_OK_DONE;

 error:
  hashcash_challenges_free(&session->hashcash_challenges, &session->n_hashcash_challenges);
  return ret;
}

static enum sp_error
handle_login5(struct sp_message *msg, struct sp_session *session)
{
  struct http_response *hres = &msg->payload.hres;
  struct sp_token *token = &session->http_accesstoken;
  Spotify__Login5__V3__LoginResponse *response = NULL;
  int ret;
  int i;

  if (hres->code != HTTP_OK)
    RETURN_ERROR(SP_ERR_INVALID, "Request to login5 returned an error");

  response = spotify__login5__v3__login_response__unpack(NULL, hres->body_len, hres->body);
  if (!response)
    RETURN_ERROR(SP_ERR_INVALID, "Could not parse login5 response");

  for (i = 0; i < response->n_warnings; i++)
    sp_cb.logmsg("Got login5 warning '%s'", err2txt(response->warnings[i], sp_login5_warning_map, ARRAY_SIZE(sp_login5_warning_map)));

  switch (response->response_case)
    {
      case SPOTIFY__LOGIN5__V3__LOGIN_RESPONSE__RESPONSE_OK:
	ret = snprintf(token->value, sizeof(token->value), "%s", response->ok->access_token);
	if (ret < 0 || ret >= sizeof(token->value))
	  RETURN_ERROR(SP_ERR_INVALID, "Unexpected access_token length");

	token->expires_after_seconds = response->ok->access_token_expires_in;
	token->received_ts = time(NULL);
	break;
      case SPOTIFY__LOGIN5__V3__LOGIN_RESPONSE__RESPONSE_CHALLENGES:
	sp_cb.logmsg("Login %zu challenges\n", response->challenges->n_challenges);
	ret = handle_login5_challenges(response->challenges, response->login_context.data, response->login_context.len, session);
	if (ret != SP_OK_DONE)
	  goto error;
	break;
      case SPOTIFY__LOGIN5__V3__LOGIN_RESPONSE__RESPONSE_ERROR:
	RETURN_ERROR(SP_ERR_LOGINFAILED, err2txt(response->error, sp_login5_error_map, ARRAY_SIZE(sp_login5_error_map)));
      default:
	RETURN_ERROR(SP_ERR_LOGINFAILED, "Login5 failed with unknown error type");
    }

  spotify__login5__v3__login_response__free_unpacked(response, NULL);
  return SP_OK_DONE;

 error:
  spotify__login5__v3__login_response__free_unpacked(response, NULL);
  return ret;
}

// If we don't get a proper response we fall back to requesting extended
// metadata, thus this function fails since that would abort the sequence
static enum sp_error
handle_metadata_get(struct sp_message *msg, struct sp_session *session)
{
  struct http_response *hres = &msg->payload.hres;
  struct sp_channel *channel = session->now_streaming_channel;
  Track *response = NULL;
  int ret;

  if (hres->code != HTTP_OK)
    goto fallback;

  // Also works for Episode response
  response = track__unpack(NULL, hres->body_len, hres->body);
  if (!response)
    goto fallback;

  ret = file_select(channel->file.id, sizeof(channel->file.id), response, session->bitrate_preferred);
  if (ret < 0)
    goto fallback;

  track__free_unpacked(response, NULL);
  return SP_OK_DONE;

 fallback:
  sp_cb.logmsg("Couldn't find file id in metadata response, will request extended metadata\n");

  track__free_unpacked(response, NULL);
  return SP_OK_DONE;
}

// We need to find the file.id (necessary to get the audio key) which is buried
// deep in the extended metadata response. Originally, it was also included in
// a metadata request, but that was broken by Spotify in spclient responses,
// except, weirdly, when requesting spclient.wg.spotify.com. The below method
// should match what go-librespot does.
static enum sp_error
handle_extended_metadata_get(struct sp_message *msg, struct sp_session *session)
{
  struct http_response *hres = &msg->payload.hres;
  struct sp_channel *channel = session->now_streaming_channel;
  Spotify__Extendedmetadata__BatchedExtensionResponse *response = NULL;
  Spotify__Extendedmetadata__EntityExtensionData *entity_extension_data = NULL;
  Track *track = NULL;
  int i, j;
  int ret;

  if (hres->code != HTTP_OK)
    RETURN_ERROR(SP_ERR_INVALID, "Request for extended metadata returned a http error");

  response = spotify__extendedmetadata__batched_extension_response__unpack(NULL, hres->body_len, hres->body);
  if (!response)
    RETURN_ERROR(SP_ERR_INVALID, "Could not parse extended metadata response");

  for (i = 0; i < response->n_extended_metadata && !entity_extension_data; i++)
    {
      for (j = 0; j < response->extended_metadata[i]->n_extension_data && !entity_extension_data; j++)
	{
	  entity_extension_data = response->extended_metadata[i]->extension_data[j];
	  if (!entity_extension_data)
	    continue;
	  else if (!entity_extension_data->entity_uri || strcmp(entity_extension_data->entity_uri, channel->file.path) != 0)
	    entity_extension_data = NULL;
	  else if (!entity_extension_data->header || entity_extension_data->header->status_code != HTTP_OK)
	    entity_extension_data = NULL;
	  else if (!entity_extension_data->extension_data || !entity_extension_data->extension_data->type_url)
	    entity_extension_data = NULL;
	}
    }

  if (!entity_extension_data)
    RETURN_ERROR(SP_ERR_INVALID, "Could not extract entity extension data from extended metadata response");

  // Like go-librespot, we don't check entity_extension_data->extension_data->type_url,
  // which should be either type.googleapis.com/spotify.metadata.Track or
  // .Episode. If we get something else we will fail later anyway.

  // This also works for episodes
  track = track__unpack(NULL, entity_extension_data->extension_data->value.len, entity_extension_data->extension_data->value.data);
  if (!track)
    RETURN_ERROR(SP_ERR_INVALID, "Could not parse track data in extended metadata response");

  ret = file_select(channel->file.id, sizeof(channel->file.id), track, session->bitrate_preferred);
  if (ret < 0)
    RETURN_ERROR(SP_ERR_INVALID, "Could not find track data in extended metadata response");

  spotify__extendedmetadata__batched_extension_response__free_unpacked(response, NULL);
  track__free_unpacked(track, NULL);
  return SP_OK_DONE;

 error:
  spotify__extendedmetadata__batched_extension_response__free_unpacked(response, NULL);
  track__free_unpacked(track, NULL);
  return ret;
}

static enum sp_error
handle_storage_resolve(struct sp_message *msg, struct sp_session *session)
{
  struct http_response *hres = &msg->payload.hres;
  Spotify__Download__Proto__StorageResolveResponse *response = NULL;
  struct sp_channel *channel = session->now_streaming_channel;
  int i;
  int ret;

  if (hres->code != HTTP_OK)
    RETURN_ERROR(SP_ERR_INVALID, "Request to storage-resolve returned an error");

  response = spotify__download__proto__storage_resolve_response__unpack(NULL, hres->body_len, hres->body);
  if (!response)
    RETURN_ERROR(SP_ERR_INVALID, "Could not parse storage-resolve response");

  switch (response->result)
    {
      case SPOTIFY__DOWNLOAD__PROTO__STORAGE_RESOLVE_RESPONSE__RESULT__CDN:
        for (i = 0; i < response->n_cdnurl && i < ARRAY_SIZE(channel->file.cdnurl); i++)
          channel->file.cdnurl[i] = strdup(response->cdnurl[i]);
        break;
      case SPOTIFY__DOWNLOAD__PROTO__STORAGE_RESOLVE_RESPONSE__RESULT__STORAGE:
        RETURN_ERROR(SP_ERR_INVALID, "Track not available via CDN storage");
      case SPOTIFY__DOWNLOAD__PROTO__STORAGE_RESOLVE_RESPONSE__RESULT__RESTRICTED:
        RETURN_ERROR(SP_ERR_INVALID, "Can't resolve storage, track access restricted");
      default:
        RETURN_ERROR(SP_ERR_INVALID, "Can't resolve storage, unknown error");
    }

  spotify__download__proto__storage_resolve_response__free_unpacked(response, NULL);
  return SP_OK_DONE;

 error:
  spotify__download__proto__storage_resolve_response__free_unpacked(response, NULL);
  return ret;
}

static int
file_size_get(struct sp_channel *channel, struct http_response *hres)
{
  char *content_range;
  const char *colon;
  int sz;

  content_range = http_response_header_find("Content-Range", hres);
  if (!content_range || !(colon = strchr(content_range, '/')))
    return -1;

  sz = atoi(colon + 1);
  if (sz <= 0)
    return -1;

  channel->file.len_bytes = sz;
  return 0;
}

// Ref. chunked_reader.go
static enum sp_error
handle_media_get(struct sp_message *msg, struct sp_session *session)
{
  struct http_response *hres = &msg->payload.hres;
  struct sp_channel *channel = session->now_streaming_channel;
  int ret;

  if (hres->code != HTTP_PARTIALCONTENT)
    RETURN_ERROR(SP_ERR_NOCONNECTION, "Request for Spotify media returned an error");

  if (channel->file.len_bytes == 0 && file_size_get(channel, hres) < 0)
    RETURN_ERROR(SP_ERR_INVALID, "Invalid content-range, can't determine media size");

//  sp_cb.logmsg("Received %zu bytes, size is %d\n", hres->body_len, channel->file.len_bytes);

  // Not sure if the channel concept even makes sense for http, but nonetheless
  // we use it to stay consistent with the old tcp protocol
  ret = channel_http_body_read(channel, hres->body, hres->body_len);
  if (ret < 0)
    goto error;

  // Save any audio data to a buffer that will be written to audio_fd[1] when
  // it is writable. Note that request for next chunk will also happen then.
  evbuffer_add(channel->audio_buf, channel->body.data, channel->body.data_len);

  return SP_OK_DATA;

 error:
  return ret;
}

static enum sp_error
handle_tcp_generic(struct sp_message *msg, struct sp_session *session)
{
  uint8_t *data = msg->payload.tmsg.data;
  size_t data_len = msg->payload.tmsg.len;
  enum sp_cmd_type cmd = data[0];
  uint8_t *payload = data + 3;
  size_t payload_len = data_len - 3 - 4;
  int ret;

  switch (cmd)
    {
      case CmdAPWelcome:
	ret = handle_apwelcome(payload, payload_len, session);
	break;
      case CmdAuthFailure:
	ret = handle_aplogin_failed(payload, payload_len, session);
	break;
      case CmdPing:
	ret = handle_ping(payload, payload_len, session);
	break;
      case CmdStreamChunkRes:
	ret = handle_chunk_res(payload, payload_len, session);
	break;
      case CmdCountryCode:
	memcpy(session->country, payload, sizeof(session->country) - 1);
	ret = SP_OK_OTHER;
	break;
      case CmdAesKey:
	ret = handle_aes_key(payload, payload_len, session);
	break;
      case CmdAesKeyError:
	ret = handle_aes_key_error(payload, payload_len, session);
	break;
      case CmdMercuryReq:
	ret = handle_mercury_req(payload, payload_len, session);
	break;
      case CmdChannelError:
        ret = handle_channel_error(payload, payload_len, session);
        break;
      case CmdLegacyWelcome: // 0 bytes, ignored by librespot
      case CmdSecretBlock: // ignored by librespot
      case 0x50: // XML received after login, ignored by librespot
      case CmdLicenseVersion: // ignored by librespot
      default:
	ret = SP_OK_OTHER;
    }

  return ret;
}

static enum sp_error
msg_tcp_handle(struct sp_message *msg, struct sp_session *session)
{
  struct sp_seq_request *request = session->request;

  // We have a tcp request waiting for a response
  if (request && request->proto == SP_PROTO_TCP && request->response_handler)
    {
//      sp_cb.logmsg("Handling response to %s\n", request->name);
      return request->response_handler(msg, session);
    }

//  sp_cb.logmsg("Handling incoming tcp message\n");
  // Not waiting for anything, could be a ping
  return handle_tcp_generic(msg, session);
}

static enum sp_error
msg_http_handle(struct sp_message *msg, struct sp_session *session)
{
  struct sp_seq_request *request = session->request;

  // We have a http request waiting for a response
  if (request && request->proto == SP_PROTO_HTTP && request->response_handler)
    {
//      sp_cb.logmsg("Handling response to %s\n", request->name);
      return request->response_handler(msg, session);
    }

  sp_errmsg = "Received unexpected http response";
  return SP_ERR_INVALID;
}

// Handler must return SP_OK_DONE if the message is a response to a request.
// It must return SP_OK_OTHER if the message is something else (e.g. a ping),
// SP_ERR_xxx if the response indicates an error. Finally, SP_OK_DATA is like
// DONE except it also means that there is new audio data to write.
enum sp_error
msg_handle(struct sp_message *msg, struct sp_session *session)
{
  if (msg->type == SP_MSG_TYPE_TCP)
    return msg_tcp_handle(msg, session);
  else if (msg->type == SP_MSG_TYPE_HTTP_RES)
    return msg_http_handle(msg, session);

  sp_errmsg = "Invalid message passed to msg_handle()";
  return SP_ERR_INVALID;
}

enum sp_error
msg_tcp_read_one(struct sp_tcp_message *tmsg, struct sp_connection *conn)
{
  size_t in_len = evbuffer_get_length(conn->incoming);
  uint8_t *in = evbuffer_pullup(conn->incoming, -1);
  uint32_t be32;
  ssize_t msg_len;
  int ret;

#ifdef DEBUG_MOCK
  if (conn->is_encrypted && !conn->decrypt.key[0] && !conn->decrypt.key[1])
    {
      uint16_t be;
      memcpy(&be, in + 1, sizeof(be));
      msg_len = be16toh(be) + 7;
      if (msg_len > in_len)
	return SP_OK_WAIT;

      tmsg->data = malloc(msg_len);
      tmsg->len = msg_len;
      evbuffer_remove(conn->incoming, tmsg->data, msg_len);

      return SP_OK_DONE;
    }
#endif

  if (conn->is_encrypted)
    {
      msg_len = crypto_decrypt(in, in_len, &conn->decrypt);
      if (msg_len < 0)
	RETURN_ERROR(SP_ERR_DECRYPTION, "Decryption error");
      if (msg_len == 0)
	return SP_OK_WAIT;
    }
  else
    {
      if (in_len < sizeof(be32))
	return SP_OK_WAIT; // Wait for more data, size header is incomplete

      memcpy(&be32, in, sizeof(be32));
      msg_len = be32toh(be32);
      if (msg_len < 0)
	RETURN_ERROR(SP_ERR_INVALID, "Invalid message length");
      if (msg_len > in_len)
	return SP_OK_WAIT;

      if (!conn->handshake_completed)
	evbuffer_add(conn->handshake_packets, in, msg_len);
    }

  // At this point we have a complete, decrypted message.
  tmsg->data = malloc(msg_len);
  tmsg->len = msg_len;
  evbuffer_remove(conn->incoming, tmsg->data, msg_len);

  return SP_OK_DONE;

 error:
  return ret;
}


/* --------------------------- Outgoing messages ---------------------------- */

static int
msg_make_ap_resolve(struct sp_message *msg, struct sp_session *session)
{
  struct http_request *hreq = &msg->payload.hreq;

  if (!must_resolve(&session->accesspoint) && !must_resolve(&session->spclient) && !must_resolve(&session->dealer))
    return 1; // Skip

  hreq->url = strdup("https://apresolve.spotify.com/?type=accesspoint&type=spclient&type=dealer");
  return 0;
}

// This message is constructed like librespot does it, see handshake.rs
static int
msg_make_client_hello(struct sp_message *msg, struct sp_session *session)
{
  struct sp_tcp_message *tmsg = &msg->payload.tmsg;
  ClientHello client_hello = CLIENT_HELLO__INIT;
  BuildInfo build_info = BUILD_INFO__INIT;
  LoginCryptoHelloUnion login_crypto = LOGIN_CRYPTO_HELLO_UNION__INIT;
  LoginCryptoDiffieHellmanHello diffie_hellman = LOGIN_CRYPTO_DIFFIE_HELLMAN_HELLO__INIT;
  Cryptosuite crypto_suite = CRYPTOSUITE__CRYPTO_SUITE_SHANNON;
  uint8_t padding[1] = { 0x1e };
  uint8_t nonce[16] = { 0 };

  build_info.product = PRODUCT__PRODUCT_PARTNER;
  build_info.platform = PLATFORM__PLATFORM_LINUX_X86;
  build_info.version = 109800078;

  diffie_hellman.gc.len = sizeof(session->conn.keys.public_key);
  diffie_hellman.gc.data = session->conn.keys.public_key;
  diffie_hellman.server_keys_known = 1;

  login_crypto.diffie_hellman = &diffie_hellman;

  client_hello.build_info = &build_info;
  client_hello.n_cryptosuites_supported = 1;
  client_hello.cryptosuites_supported = &crypto_suite;
  client_hello.login_crypto_hello = &login_crypto;
  client_hello.client_nonce.len = sizeof(nonce);
  client_hello.client_nonce.data = nonce;
  client_hello.has_padding = 1;
  client_hello.padding.len = sizeof(padding);
  client_hello.padding.data = padding;

  tmsg->len = client_hello__get_packed_size(&client_hello);
  tmsg->data = malloc(tmsg->len);

  client_hello__pack(&client_hello, tmsg->data);

  tmsg->add_version_header = true;

  return 0;
}

static int
client_response_crypto(uint8_t **challenge, size_t *challenge_len, struct sp_connection *conn)
{
  uint8_t *packets;
  size_t packets_len;
  int ret;

  packets_len = evbuffer_get_length(conn->handshake_packets);
  packets = malloc(packets_len);
  evbuffer_remove(conn->handshake_packets, packets, packets_len);

  ret = crypto_challenge(challenge, challenge_len,
                         conn->encrypt.key, sizeof(conn->encrypt.key),
                         conn->decrypt.key, sizeof(conn->decrypt.key),
                         packets, packets_len,
                         conn->keys.shared_secret, conn->keys.shared_secret_len);

  free(packets);

  return ret;
}

static int
msg_make_client_response_plaintext(struct sp_message *msg, struct sp_session *session)
{
  struct sp_tcp_message *tmsg = &msg->payload.tmsg;
  ClientResponsePlaintext client_response = CLIENT_RESPONSE_PLAINTEXT__INIT;
  LoginCryptoResponseUnion login_crypto_response = LOGIN_CRYPTO_RESPONSE_UNION__INIT;
  LoginCryptoDiffieHellmanResponse diffie_hellman = LOGIN_CRYPTO_DIFFIE_HELLMAN_RESPONSE__INIT;
  uint8_t *challenge;
  size_t challenge_len;
  int ret;

  ret = client_response_crypto(&challenge, &challenge_len, &session->conn);
  if (ret < 0)
    return -1;

  diffie_hellman.hmac.len = challenge_len;
  diffie_hellman.hmac.data = challenge;

  login_crypto_response.diffie_hellman = &diffie_hellman;

  client_response.login_crypto_response = &login_crypto_response;

  tmsg->len = client_response_plaintext__get_packed_size(&client_response);
  tmsg->data = malloc(tmsg->len);

  client_response_plaintext__pack(&client_response, tmsg->data);

  free(challenge);
  return 0;
}

static int
msg_make_client_response_encrypted(struct sp_message *msg, struct sp_session *session)
{
  struct sp_tcp_message *tmsg = &msg->payload.tmsg;
  ClientResponseEncrypted client_response = CLIENT_RESPONSE_ENCRYPTED__INIT;
  LoginCredentials login_credentials = LOGIN_CREDENTIALS__INIT;
  SystemInfo system_info = SYSTEM_INFO__INIT;
  char system_information_string[64];
  char version_string[64];

  login_credentials.has_auth_data = 1;
  login_credentials.username = session->credentials.username;

  if (session->credentials.stored_cred_len > 0)
    {
      login_credentials.typ = AUTHENTICATION_TYPE__AUTHENTICATION_STORED_SPOTIFY_CREDENTIALS;
      login_credentials.auth_data.len = session->credentials.stored_cred_len;
      login_credentials.auth_data.data = session->credentials.stored_cred;
    }
  else if (session->credentials.token_len > 0)
    {
      login_credentials.typ = AUTHENTICATION_TYPE__AUTHENTICATION_SPOTIFY_TOKEN;
      login_credentials.auth_data.len = session->credentials.token_len;
      login_credentials.auth_data.data = session->credentials.token;
    }
  else if (strlen(session->credentials.password))
    {
      login_credentials.typ = AUTHENTICATION_TYPE__AUTHENTICATION_USER_PASS;
      login_credentials.auth_data.len = strlen(session->credentials.password);
      login_credentials.auth_data.data = (unsigned char *)session->credentials.password;
    }
  else
    return -1;

  snprintf(system_information_string, sizeof(system_information_string), "%s_%s_%s",
    sp_sysinfo.client_name, sp_sysinfo.client_version, sp_sysinfo.client_build_id);
  snprintf(version_string, sizeof(version_string), "%s-%s",
    sp_sysinfo.client_name, sp_sysinfo.client_version);

  system_info.cpu_family = CPU_FAMILY__CPU_UNKNOWN;
  system_info.os = OS__OS_UNKNOWN;
  system_info.system_information_string = system_information_string;
  system_info.device_id = sp_sysinfo.device_id;
  system_info_from_uname(&system_info); // Sets cpu_family and os to actual values

  client_response.login_credentials = &login_credentials;
  client_response.system_info = &system_info;
  client_response.version_string = version_string;

  tmsg->len = client_response_encrypted__get_packed_size(&client_response);
  tmsg->data = malloc(tmsg->len);

  client_response_encrypted__pack(&client_response, tmsg->data);

  tmsg->cmd = CmdLogin;
  tmsg->encrypt = true;

  return 0;
}

// From librespot-golang:
// Mercury is the protocol implementation for Spotify Connect playback control and metadata fetching. It works as a
// PUB/SUB system, where you, as an audio sink, subscribes to the events of a specified user (playlist changes) but
// also access various metadata normally fetched by external players (tracks metadata, playlists, artists, etc).
static int
msg_make_mercury_req(size_t *total_len, uint8_t *out, size_t out_len, struct sp_mercury *mercury)
{
  Header header = HEADER__INIT;
  uint8_t *ptr;
  uint16_t be;
  uint64_t be64;
  uint8_t flags = 1; // Flags "final" according to librespot
  size_t prefix_len;
  size_t header_len;
  size_t body_len;
  int i;

  prefix_len = sizeof(be) + sizeof(be64) + sizeof(flags) + sizeof(be) + sizeof(be);

  if (prefix_len > out_len)
    return -1; // Buffer too small

  ptr = out;

  be = htobe16(sizeof(be64));
  memcpy(ptr, &be, sizeof(be)); // prefix_len += sizeof(be)
  ptr += sizeof(be);

  be64 = htobe64(mercury->seq);
  memcpy(ptr, &be64, sizeof(be64)); // prefix_len += sizeof(be64)
  ptr += sizeof(be64);

  memcpy(ptr, &flags, sizeof(flags)); // prefix_len += sizeof(flags)
  ptr += sizeof(flags);

  be = htobe16(1 + mercury->parts_num); // = payload_len + 1 = "parts count"?
  memcpy(ptr, &be, sizeof(be)); // prefix_len += sizeof(be)
  ptr += sizeof(be);

  header.uri = mercury->uri;
  header.method = mercury->method; // "GET", "SUB" etc
  header.content_type = mercury->content_type;

  header_len = header__get_packed_size(&header);
  if (header_len + prefix_len > out_len)
    return -1; // Buffer too small

  be = htobe16(header_len);
  memcpy(ptr, &be, sizeof(be)); // prefix_len += sizeof(be)
  ptr += sizeof(be);

  assert(ptr - out == prefix_len);

  header__pack(&header, ptr);
  ptr += header_len;

  body_len = 0;
  for (i = 0; i < mercury->parts_num; i++)
    {
      body_len += sizeof(be) + mercury->parts[i].len;
      if (body_len + header_len + prefix_len > out_len)
	return -1; // Buffer too small

      be = htobe16(mercury->parts[i].len);
      memcpy(ptr, &be, sizeof(be));
      ptr += sizeof(be);

      memcpy(ptr, mercury->parts[i].data, mercury->parts[i].len);
      ptr += mercury->parts[i].len;
    }

  assert(ptr - out == header_len + prefix_len + body_len);

  *total_len = header_len + prefix_len + body_len;
  return 0;
}

static int
msg_make_mercury_track_get(struct sp_message *msg, struct sp_session *session)
{
  struct sp_tcp_message *tmsg = &msg->payload.tmsg;
  struct sp_mercury mercury = { 0 };
  struct sp_channel *channel = session->now_streaming_channel;
  char uri[256];
  char *ptr;
  int i;

  assert(sizeof(uri) > sizeof(SP_MERCURY_URI_TRACK) + 2 * sizeof(channel->file.media_id));

  ptr = uri;
  ptr += sprintf(ptr, "%s", SP_MERCURY_URI_TRACK);

  for (i = 0; i < sizeof(channel->file.media_id); i++)
    ptr += sprintf(ptr, "%02x", channel->file.media_id[i]);

  mercury.method = "GET";
  mercury.seq    = channel->id;
  mercury.uri    = uri;

  tmsg->data = malloc(MERCURY_REQ_SIZE_MAX);
  tmsg->cmd = CmdMercuryReq;
  tmsg->encrypt = true;

  return msg_make_mercury_req(&tmsg->len, tmsg->data, MERCURY_REQ_SIZE_MAX, &mercury);
}

static int
msg_make_mercury_episode_get(struct sp_message *msg, struct sp_session *session)
{
  struct sp_tcp_message *tmsg = &msg->payload.tmsg;
  struct sp_mercury mercury = { 0 };
  struct sp_channel *channel = session->now_streaming_channel;
  char uri[256];
  char *ptr;
  int i;

  assert(sizeof(uri) > sizeof(SP_MERCURY_URI_EPISODE) + 2 * sizeof(channel->file.media_id));

  ptr = uri;
  ptr += sprintf(ptr, "%s", SP_MERCURY_URI_EPISODE);

  for (i = 0; i < sizeof(channel->file.media_id); i++)
    ptr += sprintf(ptr, "%02x", channel->file.media_id[i]);

  mercury.method = "GET";
  mercury.seq    = channel->id;
  mercury.uri    = uri;

  tmsg->data = malloc(MERCURY_REQ_SIZE_MAX);
  tmsg->cmd = CmdMercuryReq;
  tmsg->encrypt = true;

  return msg_make_mercury_req(&tmsg->len, tmsg->data, MERCURY_REQ_SIZE_MAX, &mercury);
}

static int
msg_make_mercury_metadata_get(struct sp_message *msg, struct sp_session *session)
{
  struct sp_channel *channel = session->now_streaming_channel;

  if (channel->file.media_type == SP_MEDIA_TRACK)
    return msg_make_mercury_track_get(msg, session);
  else if (channel->file.media_type == SP_MEDIA_EPISODE)
    return msg_make_mercury_episode_get(msg, session);

  return -1;
}

static int
msg_make_audio_key_get(struct sp_message *msg, struct sp_session *session)
{
  struct sp_tcp_message *tmsg = &msg->payload.tmsg;
  struct sp_channel *channel = session->now_streaming_channel;
  uint8_t *ptr;
  uint32_t be32;
  uint16_t be;

  tmsg->len = sizeof(channel->file.id) + sizeof(channel->file.media_id) + sizeof(be32) + sizeof(be);
  tmsg->data = malloc(tmsg->len);

  ptr = tmsg->data;

  memcpy(ptr, channel->file.id, sizeof(channel->file.id));
  ptr += sizeof(channel->file.id);

  memcpy(ptr, channel->file.media_id, sizeof(channel->file.media_id));
  ptr += sizeof(channel->file.media_id);

  be32 = htobe32(channel->id);
  memcpy(ptr, &be32, sizeof(be32));
  ptr += sizeof(be32);

  be = htobe16(0); // Unknown
  memcpy(ptr, &be, sizeof(be));
  ptr += sizeof(be);

  tmsg->cmd = CmdRequestKey;
  tmsg->encrypt = true;

  return 0;
}

static int
msg_make_chunk_request(struct sp_message *msg, struct sp_session *session)
{
  struct sp_tcp_message *tmsg = &msg->payload.tmsg;
  struct sp_channel *channel = session->now_streaming_channel;
  uint8_t *ptr;
  uint16_t be;
  uint32_t be32;

  if (!channel)
    return -1;

  tmsg->len = 3 * sizeof(be) + sizeof(channel->file.id) + 5 * sizeof(be32);
  tmsg->data = malloc(tmsg->len);

  ptr = tmsg->data;

  be = htobe16(channel->id);
  memcpy(ptr, &be, sizeof(be));
  ptr += sizeof(be); // x1

  be = htobe16(1); // Unknown purpose
  memcpy(ptr, &be, sizeof(be));
  ptr += sizeof(be); // x2

  be = htobe16(0); // Unknown purpose
  memcpy(ptr, &be, sizeof(be));
  ptr += sizeof(be); // x3

  be32 = htobe32(0); // Unknown purpose
  memcpy(ptr, &be32, sizeof(be32));
  ptr += sizeof(be32); // x1

  be32 = htobe32(0x00009C40); // Unknown purpose
  memcpy(ptr, &be32, sizeof(be32));
  ptr += sizeof(be32); // x2

  be32 = htobe32(0x00020000); // Unknown purpose
  memcpy(ptr, &be32, sizeof(be32));
  ptr += sizeof(be32); // x3

  memcpy(ptr, channel->file.id, sizeof(channel->file.id));
  ptr += sizeof(channel->file.id);

  be32 = htobe32(channel->file.offset_bytes / 4);
  memcpy(ptr, &be32, sizeof(be32));
  ptr += sizeof(be32); // x4

  be32 = htobe32(channel->file.offset_bytes / 4 + SP_CHUNK_LEN / 4);
  memcpy(ptr, &be32, sizeof(be32));
  ptr += sizeof(be32); // x5

  assert(tmsg->len == ptr - tmsg->data);
  assert(tmsg->len == 46);

  tmsg->cmd = CmdStreamChunk;
  tmsg->encrypt = true;

  return 0;
}

static int
msg_make_pong(struct sp_message *msg, struct sp_session *session)
{
  struct sp_tcp_message *tmsg = &msg->payload.tmsg;

  tmsg->len = 4;
  tmsg->data = calloc(1, tmsg->len); // librespot just replies with zeroes

  tmsg->cmd = CmdPong;
  tmsg->encrypt = true;

  return 0;
}

// Ref. session/clienttoken.go
static int
msg_make_clienttoken(struct sp_message *msg, struct sp_session *session)
{
  struct http_request *hreq = &msg->payload.hreq;
  Spotify__Clienttoken__Http__V0__ClientTokenRequest treq = SPOTIFY__CLIENTTOKEN__HTTP__V0__CLIENT_TOKEN_REQUEST__INIT;
  Spotify__Clienttoken__Http__V0__ClientDataRequest dreq = SPOTIFY__CLIENTTOKEN__HTTP__V0__CLIENT_DATA_REQUEST__INIT;
  Spotify__Clienttoken__Data__V0__ConnectivitySdkData sdk_data = SPOTIFY__CLIENTTOKEN__DATA__V0__CONNECTIVITY_SDK_DATA__INIT;
  Spotify__Clienttoken__Data__V0__PlatformSpecificData platform_data = SPOTIFY__CLIENTTOKEN__DATA__V0__PLATFORM_SPECIFIC_DATA__INIT;
  struct sp_token *token = &session->http_clienttoken;
  time_t now = time(NULL);
  bool must_refresh;

  must_refresh = (now > token->received_ts + token->expires_after_seconds) || (now > token->received_ts + token->refresh_after_seconds);
  if (!must_refresh)
    return 1; // We have a valid token, tell caller to go to next request

#ifdef HAVE_SYS_UTSNAME_H
  Spotify__Clienttoken__Data__V0__NativeDesktopMacOSData desktop_macos = SPOTIFY__CLIENTTOKEN__DATA__V0__NATIVE_DESKTOP_MAC_OSDATA__INIT;
  Spotify__Clienttoken__Data__V0__NativeDesktopLinuxData desktop_linux = SPOTIFY__CLIENTTOKEN__DATA__V0__NATIVE_DESKTOP_LINUX_DATA__INIT;
  struct utsname uts = { 0 };

  uname(&uts);
  if (strcmp(uts.sysname, "Linux") == 0)
    {
      desktop_linux.system_name = uts.sysname;
      desktop_linux.system_release = uts.release;
      desktop_linux.system_version = uts.version;
      desktop_linux.hardware = uts.machine;
      platform_data.desktop_linux = &desktop_linux;
      platform_data.data_case = SPOTIFY__CLIENTTOKEN__DATA__V0__PLATFORM_SPECIFIC_DATA__DATA_DESKTOP_LINUX;
    }
  else if (strcmp(uts.sysname, "Darwin") == 0)
    {
      desktop_macos.system_version = uts.version;
      desktop_macos.hw_model = uts.machine;
      desktop_macos.compiled_cpu_type = uts.machine;
      platform_data.desktop_macos = &desktop_macos;
      platform_data.data_case = SPOTIFY__CLIENTTOKEN__DATA__V0__PLATFORM_SPECIFIC_DATA__DATA_DESKTOP_MACOS;
    }
#endif

  sdk_data.platform_specific_data = &platform_data;
  sdk_data.device_id = sp_sysinfo.device_id; // e.g. "bcbae1f3062baac486045f13935c6c95ad4191ff"

  dreq.connectivity_sdk_data = &sdk_data;
  dreq.data_case = SPOTIFY__CLIENTTOKEN__HTTP__V0__CLIENT_DATA_REQUEST__DATA_CONNECTIVITY_SDK_DATA;
  dreq.client_version = sp_sysinfo.client_version; // e.g. "0.0.0" (SpotifyLikeClient)
  dreq.client_id = sp_sysinfo.client_id;

  treq.client_data = &dreq;
  treq.request_type = SPOTIFY__CLIENTTOKEN__HTTP__V0__CLIENT_TOKEN_REQUEST_TYPE__REQUEST_CLIENT_DATA_REQUEST;
  treq.request_case = SPOTIFY__CLIENTTOKEN__HTTP__V0__CLIENT_TOKEN_REQUEST__REQUEST_CLIENT_DATA;

  hreq->body_len = spotify__clienttoken__http__v0__client_token_request__get_packed_size(&treq);
  hreq->body = malloc(hreq->body_len);

  spotify__clienttoken__http__v0__client_token_request__pack(&treq, hreq->body);

  hreq->url = strdup("https://clienttoken.spotify.com/v1/clienttoken");

  hreq->headers[0] = strdup("Accept: application/x-protobuf");
  hreq->headers[1] = strdup("Content-Type: application/x-protobuf");

  return 0;
}

static void
challenge_solutions_clear(Spotify__Login5__V3__ChallengeSolutions *solutions)
{
  Spotify__Login5__V3__ChallengeSolution *this_solution;
  int i;

  if (!solutions->solutions)
    return;

  for (i = 0; i < solutions->n_solutions; i++)
    {
      this_solution = solutions->solutions[i];
      if (!this_solution)
	continue;

      free(this_solution->hashcash->duration);
      free(this_solution->hashcash->suffix.data);
      free(this_solution->hashcash);
      free(this_solution);
    }

  free(solutions->solutions);
}

// Finds solutions to the challenges stored in *challenges and adds them to *solutions
static int
challenge_solutions_append(Spotify__Login5__V3__ChallengeSolutions *solutions, struct crypto_hashcash_challenge *challenges, int n_challenges)
{
  Spotify__Login5__V3__ChallengeSolution *this_solution;
  struct crypto_hashcash_challenge *crypto_challenge;
  struct crypto_hashcash_solution crypto_solution;
  size_t suffix_len = sizeof(crypto_solution.suffix);
  int ret;
  int i;

  solutions->n_solutions = n_challenges;
  solutions->solutions = calloc(n_challenges, sizeof(Spotify__Login5__V3__ChallengeSolution *));
  if (!solutions->solutions)
    RETURN_ERROR(SP_ERR_OOM, "Out of memory allocating hashcash solutions");

  for (i = 0, crypto_challenge = challenges; i < n_challenges; i++, crypto_challenge++)
    {
      ret = crypto_hashcash_solve(&crypto_solution, crypto_challenge, &sp_errmsg);
      if (ret < 0)
	RETURN_ERROR(SP_ERR_INVALID, sp_errmsg);

      this_solution = malloc(sizeof(Spotify__Login5__V3__ChallengeSolution));
      spotify__login5__v3__challenge_solution__init(this_solution);
      this_solution->solution_case = SPOTIFY__LOGIN5__V3__CHALLENGE_SOLUTION__SOLUTION_HASHCASH;

      this_solution->hashcash = malloc(sizeof(Spotify__Login5__V3__Challenges__HashcashSolution));
      spotify__login5__v3__challenges__hashcash_solution__init(this_solution->hashcash);

      this_solution->hashcash->duration = malloc(sizeof(Google__Protobuf__Duration));
      google__protobuf__duration__init(this_solution->hashcash->duration);

      this_solution->hashcash->suffix.len = suffix_len;
      this_solution->hashcash->suffix.data = malloc(suffix_len);
      memcpy(this_solution->hashcash->suffix.data, crypto_solution.suffix, suffix_len);

      this_solution->hashcash->duration->seconds = crypto_solution.duration.tv_sec;
      this_solution->hashcash->duration->nanos = crypto_solution.duration.tv_nsec;

      solutions->solutions[i] = this_solution;
    }

  return 0;

 error:
  challenge_solutions_clear(solutions);
  return ret;
}

// Ref. login5/login5.go
static int
msg_make_login5(struct sp_message *msg, struct sp_session *session)
{
  struct http_request *hreq = &msg->payload.hreq;
  Spotify__Login5__V3__LoginRequest req = SPOTIFY__LOGIN5__V3__LOGIN_REQUEST__INIT;
  Spotify__Login5__V3__ChallengeSolutions solutions = SPOTIFY__LOGIN5__V3__CHALLENGE_SOLUTIONS__INIT;
  Spotify__Login5__V3__ClientInfo client_info = SPOTIFY__LOGIN5__V3__CLIENT_INFO__INIT;
  Spotify__Login5__V3__Credentials__StoredCredential stored_credential = SPOTIFY__LOGIN5__V3__CREDENTIALS__STORED_CREDENTIAL__INIT;
  struct sp_token *token = &session->http_accesstoken;
  uint8_t *login_context = NULL;
  size_t login_context_len;
  time_t now = time(NULL);
  bool must_refresh;
  int ret;

  must_refresh = (now > token->received_ts + token->expires_after_seconds);
  if (!must_refresh)
    return 1; // We have a valid token, tell caller to go to next request

  if (session->credentials.stored_cred_len == 0)
    return -1;

  // This is our second login5 request - Spotify returned challenges after the first.
  // The login_context is echoed from Spotify's response to the first login5.
  if (session->hashcash_challenges)
    {
      login_context_len = session->hashcash_challenges->ctx_len;
      login_context = malloc(login_context_len);
      memcpy(login_context, session->hashcash_challenges->ctx, login_context_len);

      ret = challenge_solutions_append(&solutions, session->hashcash_challenges, session->n_hashcash_challenges);
      hashcash_challenges_free(&session->hashcash_challenges, &session->n_hashcash_challenges);
      if (ret < 0)
	goto error;

      req.challenge_solutions = &solutions;
      req.login_context.data = login_context;
      req.login_context.len = login_context_len;
    }

  client_info.client_id = sp_sysinfo.client_id;
  client_info.device_id = sp_sysinfo.device_id;

  req.client_info = &client_info;

  stored_credential.username = session->credentials.username;
  stored_credential.data.data = session->credentials.stored_cred;
  stored_credential.data.len = session->credentials.stored_cred_len;

  req.login_method_case = SPOTIFY__LOGIN5__V3__LOGIN_REQUEST__LOGIN_METHOD_STORED_CREDENTIAL;
  req.stored_credential = &stored_credential;

  hreq->body_len = spotify__login5__v3__login_request__get_packed_size(&req);
  hreq->body = malloc(hreq->body_len);

  spotify__login5__v3__login_request__pack(&req, hreq->body);

  hreq->url = strdup("https://login5.spotify.com/v3/login");

  hreq->headers[0] = asprintf_or_die("Accept: application/x-protobuf");
  hreq->headers[1] = asprintf_or_die("Content-Type: application/x-protobuf");
  hreq->headers[2] = asprintf_or_die("Client-Token: %s", session->http_clienttoken.value);

  challenge_solutions_clear(&solutions);
  free(login_context);
  return 0;

 error:
  challenge_solutions_clear(&solutions);
  free(login_context);
  return -1;
}

static int
msg_make_login5_challenges(struct sp_message *msg, struct sp_session *session)
{
  // Spotify didn't give us any challenges during login5, so we can just proceed
  if (!session->hashcash_challenges)
    return 1; // Continue to next message

  // Otherwise make another login5 request that includes the challenge responses
  return msg_make_login5(msg, session);
}

static int
msg_make_metadata_get(struct sp_message *msg, struct sp_session *session)
{
  struct http_request *hreq = &msg->payload.hreq;
  struct sp_server *server = &session->spclient;
  struct sp_channel *channel = session->now_streaming_channel;
  const char *path;
  char *media_id = NULL;
  char *ptr;
  int i;

  if (channel->file.media_type == SP_MEDIA_TRACK)
    path = "metadata/4/track";
  else if (channel->file.media_type == SP_MEDIA_EPISODE)
    path = "metadata/4/episode";
  else
    return -1;

  media_id = malloc(2 * sizeof(channel->file.media_id) + 1);
  for (i = 0, ptr = media_id; i < sizeof(channel->file.media_id); i++)
    ptr += sprintf(ptr, "%02x", channel->file.media_id[i]);

  hreq->url = asprintf_or_die("https://%s:%d/%s/%s", server->address, server->port, path, media_id);

  hreq->headers[0] = asprintf_or_die("Accept: application/x-protobuf");
  hreq->headers[1] = asprintf_or_die("Client-Token: %s", session->http_clienttoken.value);
  hreq->headers[2] = asprintf_or_die("Authorization: Bearer %s", session->http_accesstoken.value);

  free(media_id);
  return 0;
}

// Ref. spclient/spclient.go
static int
msg_make_extended_metadata_get(struct sp_message *msg, struct sp_session *session)
{
  struct http_request *hreq = &msg->payload.hreq;
  struct sp_server *server = &session->spclient;
  Spotify__Extendedmetadata__BatchedEntityRequest req = SPOTIFY__EXTENDEDMETADATA__BATCHED_ENTITY_REQUEST__INIT;
  Spotify__Extendedmetadata__EntityRequest entity_request = SPOTIFY__EXTENDEDMETADATA__ENTITY_REQUEST__INIT;
  Spotify__Extendedmetadata__EntityRequest *entity_requests;
  Spotify__Extendedmetadata__ExtensionQuery query = SPOTIFY__EXTENDEDMETADATA__EXTENSION_QUERY__INIT;
  Spotify__Extendedmetadata__ExtensionQuery *queries;
  struct sp_channel *channel = session->now_streaming_channel;
  struct sp_file zerofile = { 0 };

  if (memcmp(channel->file.id, zerofile.id, sizeof(channel->file.id)) != 0)
    return 1; // Skip this request, we got the file id from metadata_get

  if (channel->file.media_type == SP_MEDIA_TRACK)
    query.extension_kind = SPOTIFY__EXTENDEDMETADATA__EXTENSION_KIND__TRACK_V4;
  else if (channel->file.media_type == SP_MEDIA_EPISODE)
    query.extension_kind = SPOTIFY__EXTENDEDMETADATA__EXTENSION_KIND__EPISODE_V4;
  else
    return -1;

  queries = &query;
  entity_request.query = &queries;
  entity_request.n_query = 1;
  entity_request.entity_uri = channel->file.path;

  entity_requests = &entity_request;
  req.entity_request = &entity_requests;
  req.n_entity_request = 1;

  hreq->body_len = spotify__extendedmetadata__batched_entity_request__get_packed_size(&req);
  hreq->body = malloc(hreq->body_len);

  spotify__extendedmetadata__batched_entity_request__pack(&req, hreq->body);

  hreq->url = asprintf_or_die("https://%s:%d/extended-metadata/v0/extended-metadata", server->address, server->port);

  hreq->headers[0] = asprintf_or_die("Accept: application/x-protobuf");
  hreq->headers[1] = asprintf_or_die("Client-Token: %s", session->http_clienttoken.value);
  hreq->headers[2] = asprintf_or_die("Authorization: Bearer %s", session->http_accesstoken.value);

  return 0;
}

// Resolve storage, this will just be a GET request
// Ref. spclient/spclient.go
static int
msg_make_storage_resolve(struct sp_message *msg, struct sp_session *session)
{
  struct http_request *hreq = &msg->payload.hreq;
  struct sp_server *server = &session->spclient;
  struct sp_channel *channel = session->now_streaming_channel;
  char *track_id = NULL;
  char *ptr;
  int i;

  track_id = malloc(2 * sizeof(channel->file.id) + 1);
  for (i = 0, ptr = track_id; i < sizeof(channel->file.id); i++)
    ptr += sprintf(ptr, "%02x", channel->file.id[i]);

  hreq->url = asprintf_or_die("https://%s:%d/storage-resolve/files/audio/interactive/%s", server->address, server->port, track_id);

  hreq->headers[0] = asprintf_or_die("Accept: application/x-protobuf");
  hreq->headers[1] = asprintf_or_die("Client-Token: %s", session->http_clienttoken.value);
  hreq->headers[2] = asprintf_or_die("Authorization: Bearer %s", session->http_accesstoken.value);

  free(track_id);
  return 0;
}

static int
msg_make_media_get(struct sp_message *msg, struct sp_session *session)
{
  struct http_request *hreq = &msg->payload.hreq;
  struct sp_channel *channel = session->now_streaming_channel;
  size_t bytes_from;
  size_t bytes_to;

  bytes_from = channel->file.offset_bytes;

  if (!channel->file.len_bytes || channel->file.len_bytes > channel->file.offset_bytes + SP_CHUNK_LEN)
    bytes_to = channel->file.offset_bytes + SP_CHUNK_LEN - 1;
  else
    bytes_to = channel->file.len_bytes - 1;

  hreq->url = strdup(channel->file.cdnurl[0]);

  hreq->headers[0] = asprintf_or_die("Range: bytes=%zu-%zu", bytes_from, bytes_to);

//  sp_cb.logmsg("Asking for %s\n", hreq->headers[0]);

  return 0;
}

// Must be large enough to also include null terminating elements
static struct sp_seq_request seq_requests[][15] =
{
  {
    // Just a dummy so that the array is aligned with the enum
    { SP_SEQ_STOP },
  },
  {
    // Resolve will be skipped if already done and servers haven't failed on us
    { SP_SEQ_LOGIN, "AP_RESOLVE", SP_PROTO_HTTP, msg_make_ap_resolve, NULL, handle_ap_resolve, },
    { SP_SEQ_LOGIN, "CLIENT_HELLO", SP_PROTO_TCP, msg_make_client_hello, prepare_tcp_handshake, handle_client_hello, },
    { SP_SEQ_LOGIN, "CLIENT_RESPONSE_PLAINTEXT", SP_PROTO_TCP, msg_make_client_response_plaintext, prepare_tcp_handshake, NULL, },
    { SP_SEQ_LOGIN, "CLIENT_RESPONSE_ENCRYPTED", SP_PROTO_TCP,  msg_make_client_response_encrypted, prepare_tcp_handshake, handle_tcp_generic, },
  },
  {
    // The first two will be skipped if valid tokens already exist
    { SP_SEQ_MEDIA_OPEN, "CLIENTTOKEN", SP_PROTO_HTTP, msg_make_clienttoken, NULL, handle_clienttoken, },
    { SP_SEQ_MEDIA_OPEN, "LOGIN5", SP_PROTO_HTTP, msg_make_login5, NULL, handle_login5, },
    { SP_SEQ_MEDIA_OPEN, "LOGIN5_CHALLENGES", SP_PROTO_HTTP, msg_make_login5_challenges, NULL, handle_login5, },
    { SP_SEQ_MEDIA_OPEN, "METADATA_GET", SP_PROTO_HTTP, msg_make_metadata_get, NULL, handle_metadata_get, },
    { SP_SEQ_MEDIA_OPEN, "EXTENDED_METADATA_GET", SP_PROTO_HTTP, msg_make_extended_metadata_get, NULL, handle_extended_metadata_get, },
    { SP_SEQ_MEDIA_OPEN, "AUDIO_KEY_GET", SP_PROTO_TCP, msg_make_audio_key_get, prepare_tcp, handle_tcp_generic, },
    { SP_SEQ_MEDIA_OPEN, "STORAGE_RESOLVE", SP_PROTO_HTTP, msg_make_storage_resolve, NULL, handle_storage_resolve, },
    { SP_SEQ_MEDIA_OPEN, "MEDIA_PREFETCH", SP_PROTO_HTTP, msg_make_media_get, NULL, handle_media_get, },
  },
  {
    { SP_SEQ_MEDIA_GET, "MEDIA_GET", SP_PROTO_HTTP, msg_make_media_get, NULL, handle_media_get, },
  },
  {
    { SP_SEQ_PONG, "PONG", SP_PROTO_TCP, msg_make_pong, prepare_tcp, NULL, },
  },
};

// Must be large enough to also include null terminating elements
static struct sp_seq_request seq_requests_legacy[][7] =
{
  {
    // Just a dummy so that the array is aligned with the enum
    { SP_SEQ_STOP },
  },
  {
    { SP_SEQ_LOGIN, "AP_RESOLVE", SP_PROTO_HTTP, msg_make_ap_resolve, NULL, handle_ap_resolve, },
    { SP_SEQ_LOGIN, "CLIENT_HELLO", SP_PROTO_TCP, msg_make_client_hello, prepare_tcp_handshake, handle_client_hello, },
    { SP_SEQ_LOGIN, "CLIENT_RESPONSE_PLAINTEXT", SP_PROTO_TCP, msg_make_client_response_plaintext, prepare_tcp_handshake, NULL, },
    { SP_SEQ_LOGIN, "CLIENT_RESPONSE_ENCRYPTED", SP_PROTO_TCP,  msg_make_client_response_encrypted, prepare_tcp_handshake, handle_tcp_generic, },
  },
  {
    { SP_SEQ_MEDIA_OPEN, "MERCURY_METADATA_GET", SP_PROTO_TCP, msg_make_mercury_metadata_get, prepare_tcp, handle_tcp_generic, },
    { SP_SEQ_MEDIA_OPEN, "AUDIO_KEY_GET", SP_PROTO_TCP, msg_make_audio_key_get, prepare_tcp, handle_tcp_generic, },
    { SP_SEQ_MEDIA_OPEN, "CHUNK_PREFETCH", SP_PROTO_TCP, msg_make_chunk_request, prepare_tcp, handle_tcp_generic, },
  },
  {
    { SP_SEQ_MEDIA_GET, "CHUNK_REQUEST", SP_PROTO_TCP, msg_make_chunk_request, prepare_tcp, handle_tcp_generic, },
  },
  {
    { SP_SEQ_PONG, "PONG", SP_PROTO_TCP, msg_make_pong, prepare_tcp, NULL, },
  },
};

int
seq_requests_check(void)
{
  for (int i = 0; i < ARRAY_SIZE(seq_requests); i++)
    {
      if (i != seq_requests[i]->seq_type)
	return -1;
    }
  for (int i = 0; i < ARRAY_SIZE(seq_requests_legacy); i++)
    {
      if (i != seq_requests_legacy[i]->seq_type)
	return -1;
    }

  return 0;
}

struct sp_seq_request *
seq_request_get(enum sp_seq_type seq_type, int n, bool use_legacy)
{
  if (use_legacy)
    return &seq_requests_legacy[seq_type][n];

  return &seq_requests[seq_type][n];
}

// This is just a wrapper to help debug if we are unintentionally overwriting
// a queued sequence
void
seq_next_set(struct sp_session *session, enum sp_seq_type seq_type)
{
  bool will_overwrite = (seq_type != SP_SEQ_STOP && session->next_seq != SP_SEQ_STOP && seq_type != session->next_seq);

  if (will_overwrite)
    sp_cb.logmsg("Bug! Sequence is being overwritten (prev %d, new %d)", session->next_seq, seq_type);

  assert(!will_overwrite);

  session->next_seq = seq_type;
}

enum sp_error
seq_request_prepare(struct sp_seq_request *request, struct sp_conn_callbacks *cb, struct sp_session *session)
{
  if (!request->request_prepare)
    return SP_OK_DONE;

  return request->request_prepare(request, cb, session);
}

void
msg_clear(struct sp_message *msg)
{
  if (!msg)
    return;

  if (msg->type == SP_MSG_TYPE_HTTP_REQ)
    http_request_free(&msg->payload.hreq, true);
  else if (msg->type == SP_MSG_TYPE_HTTP_RES)
    http_response_free(&msg->payload.hres, true);
  else if (msg->type == SP_MSG_TYPE_TCP)
    free(msg->payload.tmsg.data);

  memset(msg, 0, sizeof(struct sp_message));
}

int
msg_make(struct sp_message *msg, struct sp_seq_request *req, struct sp_session *session)
{
  memset(msg, 0, sizeof(struct sp_message));

  msg->type = (req->proto == SP_PROTO_HTTP) ? SP_MSG_TYPE_HTTP_REQ : SP_MSG_TYPE_TCP;

  return req->payload_make(msg, session);
}

enum sp_error
msg_tcp_send(struct sp_tcp_message *tmsg, struct sp_connection *conn)
{
  uint8_t pkt[4096];
  ssize_t pkt_len;
  int ret;

  if (conn->is_encrypted)
    pkt_len = packet_make_encrypted(pkt, sizeof(pkt), tmsg->cmd, tmsg->data, tmsg->len, &conn->encrypt);
  else
    pkt_len = packet_make_plain(pkt, sizeof(pkt), tmsg->data, tmsg->len, tmsg->add_version_header);

  if (pkt_len < 0)
    RETURN_ERROR(SP_ERR_INVALID, "Error constructing packet to Spotify");

#ifndef DEBUG_MOCK
  ret = send(conn->response_fd, pkt, pkt_len, 0);
  if (ret != pkt_len)
    RETURN_ERROR(SP_ERR_NOCONNECTION, "Error sending packet to Spotify");

//  sp_cb.logmsg("Sent pkt type %d (cmd=0x%02x) with size %zu (fd=%d)\n", tmsg->type, tmsg->cmd, pkt_len, conn->response_fd);
#else
  ret = debug_mock_response(msg, conn);
  if (ret < 0)
    RETURN_ERROR(SP_ERR_NOCONNECTION, "Error mocking send packet to Spotify");

  sp_cb.logmsg("Mocked send/response pkt type %d (cmd=0x%02x) with size %zu\n", tmsg->type, tmsg->cmd, pkt_len);
#endif

  // Save sent packet for MAC calculation later
  if (!conn->handshake_completed)
    evbuffer_add(conn->handshake_packets, pkt, pkt_len);

  // Reset the disconnect timer
  event_add(conn->idle_ev, &sp_idle_tv);

  return SP_OK_DONE;

 error:
  return ret;
}

enum sp_error
msg_http_send(struct http_response *hres, struct http_request *hreq, struct http_session *hses)
{
  int ret;

  hreq->user_agent = sp_sysinfo.client_name;

//  sp_cb.logmsg("Making http request to %s\n", hreq->url);

  ret = http_request(hres, hreq, hses);
  if (ret < 0)
    RETURN_ERROR(SP_ERR_NOCONNECTION, "No connection to Spotify for http request");

  return SP_OK_DONE;

 error:
  return ret;
}

enum sp_error
msg_pong(struct sp_session *session)
{
  struct sp_seq_request *req;
  struct sp_message msg = { 0 };
  int ret;

  req = seq_request_get(SP_SEQ_PONG, 0, session->use_legacy);

  ret = msg_make(&msg, req, session);
  if (ret < 0)
    RETURN_ERROR(SP_ERR_INVALID, "Error constructing pong message to Spotify");

  ret = msg_tcp_send(&msg.payload.tmsg, &session->conn);
  if (ret < 0)
    RETURN_ERROR(ret, sp_errmsg);

  msg_clear(&msg);

  return SP_OK_DONE;

 error:
  msg_clear(&msg);

  return ret;
}
