#include "store.h"
#include "networkmanager.h"
#include <QDebug>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply>

// ────────────────────────────────────────────────────────────────
// Operation
// ────────────────────────────────────────────────────────────────

Operation::Operation(QNetworkReply *reply, QObject *parent)
    : QObject(parent)
{
  connect(this, &Operation::finished, this, &QObject::deleteLater);

  connect(reply, &QNetworkReply::finished, this, [this, reply]() {
    reply->deleteLater();

    if (reply->error() != QNetworkReply::NoError)
    {
      QByteArray body = reply->readAll();
      QJsonDocument doc = QJsonDocument::fromJson(body);
      QString message = doc.isObject() ? doc.object().value("message").toString() : QString();
      if (message.isEmpty())
      {
        message = reply->errorString();
      }
      qWarning() << message;
      emit failed(message);
      emit finished();
      return;
    }

    QByteArray body = reply->readAll();
    QJsonDocument doc = body.isEmpty() ? QJsonDocument() : QJsonDocument::fromJson(body);
    emit success(doc);
    emit finished();
  });
}

// ────────────────────────────────────────────────────────────────
// Store — construction
// ────────────────────────────────────────────────────────────────

Store::Store(QObject *parent)
    : QObject(parent)
{
}

// ────────────────────────────────────────────────────────────────
// JSON parsing
// ────────────────────────────────────────────────────────────────

Human Store::parseHuman(const QJsonObject &obj)
{
  Human h;
  h.id = obj["id"].toString();
  h.email = obj["email"].toString();
  h.emailPrefix = obj["email_prefix"].toString();
  return h;
}

Network Store::parseNetwork(const QJsonObject &obj)
{
  Network n;
  n.id = obj["id"].toString();
  n.name = obj["name"].toString();
  n.admin = parseHuman(obj["admin_human"].toObject());
  n.openStreamCount = obj["open_stream_count"].toInt();
  n.openStreamCapacity = obj["open_stream_capacity"].toInt();

  const QJsonArray humansArr = obj["humans"].toArray();
  for (const QJsonValue &v : humansArr)
  {
    n.members.push_back(parseHuman(v.toObject()));
  }

  // Streams are parsed at a higher level (populateFromStartupData) so we can
  // also extract particles per stream. Individual fetch calls parse streams inline.
  return n;
}

Stream Store::parseStream(const QJsonObject &obj)
{
  Stream s;
  s.id = obj["id"].toString();
  s.name = obj["name"].toString();
  s.description = obj["description"].toString();
  s.isOpen = (obj["status"].toString() == "open");
  s.unseenCount = obj["unseen_count"].toInt();

  const QJsonArray membersArr = obj["members"].toArray();
  for (const QJsonValue &v : membersArr)
  {
    s.memberEmails.push_back(v.toString());
  }

  return s;
}

Particle Store::parseParticle(const QJsonObject &obj)
{
  Particle p;
  p.id = obj["id"].toString();
  p.type = obj["type"].toString();
  p.createdByEmail = obj["created_by_email"].toString();
  p.seen = obj["seen"].toBool();

  const QJsonObject dataObj = obj["data"].toObject();
  for (auto it = dataObj.begin(); it != dataObj.end(); ++it)
  {
    p.data[it.key()] = it.value().toVariant();
  }

  const QJsonArray acksArr = obj["acks"].toArray();
  for (const QJsonValue &v : acksArr)
  {
    // acks can be objects with { email, acked_at } or plain strings
    if (v.isObject())
    {
      p.ackedByEmails.push_back(v.toObject()["email"].toString());
    }
    else
    {
      p.ackedByEmails.push_back(v.toString());
    }
  }

  p.updatedAt = QDateTime::fromString(obj["updated_at"].toString(), Qt::ISODate);
  p.createdAt = QDateTime::fromString(obj["created_at"].toString(), Qt::ISODate);
  return p;
}

void Store::populateFromStartupData(const QJsonObject &root)
{
  m_networks.clear();
  m_particles.clear();
  m_networkIndex.clear();
  m_streamToNetwork.clear();

  const QJsonArray networksArr = root["networks"].toArray();
  for (const QJsonValue &netVal : networksArr)
  {
    QJsonObject netObj = netVal.toObject();
    Network network = parseNetwork(netObj);

    const QJsonArray streamsArr = netObj["streams"].toArray();
    for (const QJsonValue &streamVal : streamsArr)
    {
      QJsonObject streamObj = streamVal.toObject();
      Stream stream = parseStream(streamObj);
      network.streams.push_back(stream);
      m_streamToNetwork[stream.id] = network.id;

      // Parse particles embedded in stream
      QList<Particle> particleList;
      const QJsonArray particlesArr = streamObj["particles"].toArray();
      for (const QJsonValue &partVal : particlesArr)
      {
        particleList.append(parseParticle(partVal.toObject()));
      }
      m_particles[stream.id] = particleList;
    }

    m_networkIndex[network.id] = m_networks.size();
    m_networks.append(network);
  }
}

// ────────────────────────────────────────────────────────────────
// State mutation helpers
// ────────────────────────────────────────────────────────────────

void Store::upsertNetwork(const Network &network)
{
  if (m_networkIndex.contains(network.id))
  {
    int idx = m_networkIndex[network.id];
    std::vector<Stream> existingStreams = m_networks[idx].streams;
    m_networks[idx] = network;
    if (network.streams.empty())
    {
      m_networks[idx].streams = existingStreams;
    }
  }
  else
  {
    m_networkIndex[network.id] = m_networks.size();
    m_networks.append(network);
  }
}

void Store::upsertStream(const QString &networkId, const Stream &stream)
{
  if (!m_networkIndex.contains(networkId))
  {
    return;
  }

  auto &streams = m_networks[m_networkIndex[networkId]].streams;
  for (size_t i = 0; i < streams.size(); ++i)
  {
    if (streams[i].id == stream.id)
    {
      streams[i] = stream;
      m_streamToNetwork[stream.id] = networkId;
      return;
    }
  }
  streams.push_back(stream);
  m_streamToNetwork[stream.id] = networkId;
}

void Store::upsertParticle(const QString &streamId, const Particle &particle)
{
  QList<Particle> &list = m_particles[streamId];
  for (int i = 0; i < list.size(); ++i)
  {
    if (list[i].id == particle.id)
    {
      list[i] = particle;
      return;
    }
  }
  list.append(particle);
}

void Store::removeParticleFromState(const QString &particleId)
{
  for (auto it = m_particles.begin(); it != m_particles.end(); ++it)
  {
    QList<Particle> &list = it.value();
    for (int i = 0; i < list.size(); ++i)
    {
      if (list[i].id == particleId)
      {
        list.removeAt(i);
        return;
      }
    }
  }
}

QString Store::findStreamForParticle(const QString &particleId) const
{
  for (auto it = m_particles.constBegin(); it != m_particles.constEnd(); ++it)
  {
    for (const auto &p : it.value())
    {
      if (p.id == particleId)
      {
        return it.key();
      }
    }
  }
  return QString();
}

// ────────────────────────────────────────────────────────────────
// Accessors
// ────────────────────────────────────────────────────────────────

const QList<Network> &Store::networks() const { return m_networks; }

int Store::networkCount() const { return m_networks.size(); }

const Network *Store::networkById(const QString &id) const
{
  auto it = m_networkIndex.find(id);
  if (it == m_networkIndex.end())
  {
    return nullptr;
  }
  return &m_networks[it.value()];
}

QList<Stream> Store::streamsForNetwork(const QString &networkId) const
{
  auto it = m_networkIndex.find(networkId);
  if (it == m_networkIndex.end())
  {
    return {};
  }
  const auto &streams = m_networks[it.value()].streams;
  QList<Stream> result;
  result.reserve(static_cast<int>(streams.size()));
  for (const auto &s : streams)
  {
    result.append(s);
  }
  return result;
}

int Store::streamCount(const QString &networkId) const
{
  auto it = m_networkIndex.find(networkId);
  if (it == m_networkIndex.end())
  {
    return 0;
  }
  return static_cast<int>(m_networks[it.value()].streams.size());
}

const Stream *Store::streamById(const QString &id) const
{
  auto netIt = m_streamToNetwork.find(id);
  if (netIt == m_streamToNetwork.end())
  {
    return nullptr;
  }
  auto idxIt = m_networkIndex.find(netIt.value());
  if (idxIt == m_networkIndex.end())
  {
    return nullptr;
  }
  const auto &streams = m_networks[idxIt.value()].streams;
  for (const auto &s : streams)
  {
    if (s.id == id)
    {
      return &s;
    }
  }
  return nullptr;
}

const QList<Particle> &Store::particlesForStream(const QString &streamId) const
{
  static const QList<Particle> empty;
  auto it = m_particles.find(streamId);
  if (it == m_particles.end())
  {
    return empty;
  }
  return it.value();
}

int Store::particleCount(const QString &streamId) const
{
  return m_particles.value(streamId).size();
}

const Particle *Store::particleById(const QString &id) const
{
  for (auto it = m_particles.constBegin(); it != m_particles.constEnd(); ++it)
  {
    for (const auto &p : it.value())
    {
      if (p.id == id)
      {
        return &p;
      }
    }
  }
  return nullptr;
}

// ────────────────────────────────────────────────────────────────
// Bootstrap
// ────────────────────────────────────────────────────────────────

Operation *Store::loadStartupData()
{
  QNetworkReply *reply = NetworkManager::instance().get("/startup");
  auto *op = new Operation(reply, this);
  connect(op, &Operation::success, this, [this](const QJsonDocument &doc) {
    if (!doc.isObject())
    {
      qWarning() << "Invalid startup data";
      return;
    }
    populateFromStartupData(doc.object());
    emit startupDataLoaded();
    emit networksChanged();
  });
  return op;
}

// ────────────────────────────────────────────────────────────────
// Network operations
// ────────────────────────────────────────────────────────────────

Operation *Store::createNetwork(const QString &name)
{
  QJsonObject body;
  body["name"] = name;

  QNetworkReply *reply = NetworkManager::instance().post("/networks", QJsonDocument(body));
  auto *op = new Operation(reply, this);
  connect(op, &Operation::success, this, [this](const QJsonDocument &doc) {
    if (doc.isObject())
    {
      upsertNetwork(parseNetwork(doc.object()));
      emit networksChanged();
    }
  });
  return op;
}

Operation *Store::fetchNetworks()
{
  QNetworkReply *reply = NetworkManager::instance().get("/networks");
  auto *op = new Operation(reply, this);
  connect(op, &Operation::success, this, [this](const QJsonDocument &doc) {
    const QJsonArray arr = doc.array();
    for (const QJsonValue &v : arr)
    {
      upsertNetwork(parseNetwork(v.toObject()));
    }
    emit networksChanged();
  });
  return op;
}

Operation *Store::fetchNetwork(const QString &networkId)
{
  QNetworkReply *reply = NetworkManager::instance().get(QString("/networks/%1").arg(networkId));
  auto *op = new Operation(reply, this);
  connect(op, &Operation::success, this, [this, networkId](const QJsonDocument &doc) {
    QJsonObject obj = doc.object();
    Network network = parseNetwork(obj);

    const QJsonArray streamsArr = obj["streams"].toArray();
    for (const QJsonValue &sv : streamsArr)
    {
      Stream stream = parseStream(sv.toObject());
      network.streams.push_back(stream);
      m_streamToNetwork[stream.id] = network.id;
    }
    upsertNetwork(network);
    emit networksChanged();
  });
  return op;
}

Operation *Store::addNetworkMembers(const QString &networkId, const QStringList &emails)
{
  QJsonObject body;
  QJsonArray arr;
  for (const QString &e : emails)
  {
    arr.append(e);
  }
  body["email_addresses"] = arr;

  QNetworkReply *reply = NetworkManager::instance().post(
      QString("/networks/%1/members").arg(networkId), QJsonDocument(body));
  return new Operation(reply, this);
}

Operation *Store::removeNetworkMember(const QString &networkId, const QString &email)
{
  QNetworkReply *reply = NetworkManager::instance().del(
      QString("/networks/%1/members/%2").arg(networkId, email));
  return new Operation(reply, this);
}

Operation *Store::setStreamCapacity(const QString &networkId, int capacity)
{
  QJsonObject body;
  body["capacity"] = capacity;

  QNetworkReply *reply = NetworkManager::instance().put(
      QString("/networks/%1/capacity").arg(networkId), QJsonDocument(body));
  return new Operation(reply, this);
}

// ────────────────────────────────────────────────────────────────
// Stream operations
// ────────────────────────────────────────────────────────────────

Operation *Store::createStream(const QString &networkId, const QString &name,
                               const QString &description, const QString &visibility,
                               const QStringList &members)
{
  QJsonObject body;
  body["name"] = name;
  body["description"] = description;
  body["visibility"] = visibility;
  QJsonArray membersArr;
  for (const QString &m : members)
  {
    membersArr.append(m);
  }
  body["members"] = membersArr;

  QNetworkReply *reply = NetworkManager::instance().post(
      QString("/networks/%1/streams").arg(networkId), QJsonDocument(body));
  auto *op = new Operation(reply, this);
  connect(op, &Operation::success, this, [this, networkId](const QJsonDocument &doc) {
    if (doc.isObject())
    {
      Stream stream = parseStream(doc.object());
      upsertStream(networkId, stream);
      emit streamsChanged(networkId);
    }
  });
  return op;
}

Operation *Store::fetchStream(const QString &streamId)
{
  QNetworkReply *reply = NetworkManager::instance().get(QString("/streams/%1").arg(streamId));
  auto *op = new Operation(reply, this);
  connect(op, &Operation::success, this, [this, streamId](const QJsonDocument &doc) {
    QJsonObject obj = doc.object();
    Stream stream = parseStream(obj);

    QString networkId = m_streamToNetwork.value(streamId);
    if (!networkId.isEmpty())
    {
      upsertStream(networkId, stream);
      emit streamsChanged(networkId);
    }

    // Parse particles if present
    const QJsonArray particlesArr = obj["particles"].toArray();
    QList<Particle> particleList;
    for (const QJsonValue &pv : particlesArr)
    {
      particleList.append(parseParticle(pv.toObject()));
    }
    m_particles[streamId] = particleList;
    emit particlesChanged(streamId);
  });
  return op;
}

Operation *Store::updateStream(const QString &streamId, const QString &name, const QString &description)
{
  QJsonObject body;
  if (!name.isEmpty())
  {
    body["name"] = name;
  }
  if (!description.isEmpty())
  {
    body["description"] = description;
  }

  QNetworkReply *reply = NetworkManager::instance().patch(
      QString("/streams/%1").arg(streamId), QJsonDocument(body));
  auto *op = new Operation(reply, this);
  connect(op, &Operation::success, this, [this, streamId](const QJsonDocument &doc) {
    if (doc.isObject())
    {
      Stream stream = parseStream(doc.object());
      QString networkId = m_streamToNetwork.value(streamId);
      if (!networkId.isEmpty())
      {
        upsertStream(networkId, stream);
        emit streamsChanged(networkId);
      }
    }
  });
  return op;
}

Operation *Store::openStream(const QString &streamId)
{
  QNetworkReply *reply = NetworkManager::instance().post(
      QString("/streams/%1/open").arg(streamId), QJsonDocument());
  auto *op = new Operation(reply, this);
  connect(op, &Operation::success, this, [this, streamId](const QJsonDocument &) {
    QString networkId = m_streamToNetwork.value(streamId);
    if (m_networkIndex.contains(networkId))
    {
      auto &streams = m_networks[m_networkIndex[networkId]].streams;
      for (auto &s : streams)
      {
        if (s.id == streamId)
        {
          s.isOpen = true;
          break;
        }
      }
      emit streamsChanged(networkId);
    }
  });
  return op;
}

Operation *Store::closeStream(const QString &streamId)
{
  QNetworkReply *reply = NetworkManager::instance().post(
      QString("/streams/%1/close").arg(streamId), QJsonDocument());
  auto *op = new Operation(reply, this);
  connect(op, &Operation::success, this, [this, streamId](const QJsonDocument &) {
    QString networkId = m_streamToNetwork.value(streamId);
    if (m_networkIndex.contains(networkId))
    {
      auto &streams = m_networks[m_networkIndex[networkId]].streams;
      for (auto &s : streams)
      {
        if (s.id == streamId)
        {
          s.isOpen = false;
          break;
        }
      }
      emit streamsChanged(networkId);
    }
  });
  return op;
}

Operation *Store::addStreamMembers(const QString &streamId, const QStringList &emails)
{
  QJsonObject body;
  QJsonArray arr;
  for (const QString &e : emails)
  {
    arr.append(e);
  }
  body["emails"] = arr;

  QNetworkReply *reply = NetworkManager::instance().post(
      QString("/streams/%1/members").arg(streamId), QJsonDocument(body));
  return new Operation(reply, this);
}

Operation *Store::removeStreamMembers(const QString &streamId, const QStringList &emails)
{
  QJsonObject body;
  QJsonArray arr;
  for (const QString &e : emails)
    arr.append(e);
  body["emails"] = arr;

  QNetworkReply *reply = NetworkManager::instance().del(
      QString("/streams/%1/members").arg(streamId), QJsonDocument(body));
  return new Operation(reply, this);
}

// ────────────────────────────────────────────────────────────────
// Particle operations
// ────────────────────────────────────────────────────────────────

Operation *Store::createParticle(const QString &streamId, const QString &type,
                                 const QMap<QString, QVariant> &data)
{
  QJsonObject body;
  body["type"] = type;
  QJsonObject dataObj;
  for (auto it = data.constBegin(); it != data.constEnd(); ++it)
    dataObj[it.key()] = QJsonValue::fromVariant(it.value());
  body["data"] = dataObj;

  QNetworkReply *reply = NetworkManager::instance().post(
      QString("/streams/%1/particles").arg(streamId), QJsonDocument(body));
  auto *op = new Operation(reply, this);
  connect(op, &Operation::success, this, [this, streamId](const QJsonDocument &doc) {
    if (doc.isObject())
    {
      Particle particle = parseParticle(doc.object());
      upsertParticle(streamId, particle);
      emit particlesChanged(streamId);
    }
  });
  return op;
}

Operation *Store::fetchParticle(const QString &particleId)
{
  QNetworkReply *reply = NetworkManager::instance().get(QString("/particles/%1").arg(particleId));
  auto *op = new Operation(reply, this);
  connect(op, &Operation::success, this, [this, particleId](const QJsonDocument &doc) {
    if (doc.isObject())
    {
      Particle particle = parseParticle(doc.object());
      QString streamId = findStreamForParticle(particleId);
      if (!streamId.isEmpty())
      {
        upsertParticle(streamId, particle);
        emit particlesChanged(streamId);
      }
    }
  });
  return op;
}

Operation *Store::updateParticle(const QString &particleId, const QMap<QString, QVariant> &data)
{
  QJsonObject body;
  QJsonObject dataObj;
  for (auto it = data.constBegin(); it != data.constEnd(); ++it)
    dataObj[it.key()] = QJsonValue::fromVariant(it.value());
  body["data"] = dataObj;

  QNetworkReply *reply = NetworkManager::instance().patch(
      QString("/particles/%1").arg(particleId), QJsonDocument(body));
  auto *op = new Operation(reply, this);
  connect(op, &Operation::success, this, [this, particleId](const QJsonDocument &doc) {
    if (doc.isObject())
    {
      Particle particle = parseParticle(doc.object());
      QString streamId = findStreamForParticle(particleId);
      if (!streamId.isEmpty())
      {
        upsertParticle(streamId, particle);
        emit particlesChanged(streamId);
      }
    }
  });
  return op;
}

Operation *Store::deleteParticle(const QString &particleId)
{
  QNetworkReply *reply = NetworkManager::instance().del(QString("/particles/%1").arg(particleId));
  auto *op = new Operation(reply, this);
  connect(op, &Operation::success, this, [this, particleId](const QJsonDocument &) {
    QString streamId = findStreamForParticle(particleId);
    removeParticleFromState(particleId);
    if (!streamId.isEmpty())
      emit particlesChanged(streamId);
  });
  return op;
}

Operation *Store::markParticleSeen(const QString &particleId)
{
  // Optimistic local update
  for (auto it = m_particles.begin(); it != m_particles.end(); ++it)
  {
    for (int i = 0; i < it.value().size(); ++i)
    {
      if (it.value()[i].id == particleId && !it.value()[i].seen)
      {
        it.value()[i].seen = true;
        QString streamId = it.key();

        // Decrement the parent stream's unseen count
        QString networkId = m_streamToNetwork.value(streamId);
        if (m_networkIndex.contains(networkId))
        {
          auto &streams = m_networks[m_networkIndex[networkId]].streams;
          for (auto &s : streams)
          {
            if (s.id == streamId && s.unseenCount > 0)
            {
              s.unseenCount--;
              break;
            }
          }
        }

        emit particleUpdated(streamId, particleId);
        emit streamUpdated(streamId);
        break;
      }
    }
  }

  QNetworkReply *reply = NetworkManager::instance().post(
      QString("/particles/%1/seen").arg(particleId), QJsonDocument());
  return new Operation(reply, this);
}

Operation *Store::markParticlesSeen(const QStringList &particleIds)
{
  // Optimistic local update
  QSet<QString> affectedStreams;
  for (const QString &pid : particleIds)
  {
    for (auto it = m_particles.begin(); it != m_particles.end(); ++it)
    {
      for (int i = 0; i < it.value().size(); ++i)
      {
        if (it.value()[i].id == pid)
        {
          it.value()[i].seen = true;
          affectedStreams.insert(it.key());
          break;
        }
      }
    }
  }
  for (const QString &streamId : affectedStreams)
    emit particlesChanged(streamId);

  QJsonObject body;
  QJsonArray ids;
  for (const QString &id : particleIds)
    ids.append(id);
  body["particle_ids"] = ids;

  QNetworkReply *reply = NetworkManager::instance().post("/particles/seen", QJsonDocument(body));
  return new Operation(reply, this);
}

void Store::setCurrentUserEmail(const QString &email) { m_currentUserEmail = email; }

QString Store::currentUserEmail() const { return m_currentUserEmail; }

Operation *Store::ackParticle(const QString &particleId)
{
  // Optimistic local update
  for (auto it = m_particles.begin(); it != m_particles.end(); ++it)
  {
    for (int i = 0; i < it.value().size(); ++i)
    {
      if (it.value()[i].id == particleId)
      {
        auto &emails = it.value()[i].ackedByEmails;
        if (!m_currentUserEmail.isEmpty() &&
            std::find(emails.begin(), emails.end(), m_currentUserEmail) == emails.end())
        {
          emails.push_back(m_currentUserEmail);
          emit particleUpdated(it.key(), particleId);
        }
        break;
      }
    }
  }

  QNetworkReply *reply = NetworkManager::instance().post(
      QString("/particles/%1/ack").arg(particleId), QJsonDocument());
  return new Operation(reply, this);
}
