/*
 * Copyright (c) 2024-2025 Matthias Klumpp <matthias@tenstral.net>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; see the file COPYING.  If not, write to
 * the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

#include "deb822.h"

#include <iostream>
#include <fstream>
#include <sstream>
#include <algorithm>

Deb822File::Deb822File() {}

bool Deb822File::isFieldStanza(const Stanza &stanza)
{
    return std::any_of(stanza.begin(), stanza.end(), [](const Line &l) {
        return l.isField();
    });
}

Deb822File::Line Deb822File::parseDeb822Line(const std::string &line) const
{
    Line l;
    l.content = line;

    // we return empty and comment-lines verbatim
    if (line.empty() || line[0] == '#')
        return l;

    if (std::isspace(line[0])) {
        l.isContinuation = true;
        return l;
    }

    const auto colonPos = line.find(':');
    if (colonPos != std::string::npos && colonPos > 0) {
        l.key = line.substr(0, colonPos);

        // strip leading space
        size_t valueStart = colonPos + 1;
        while (valueStart < line.size() && std::isspace(line[valueStart]))
            ++valueStart;

        l.value = line.substr(valueStart);
    }

    return l;
}

bool Deb822File::loadFromStream(std::istream &stream)
{
    m_allStanzas.clear();
    m_fieldStanzaIndices.clear();

    Stanza stanza;
    std::string line;
    ssize_t lastFieldIdx = -1;

    while (std::getline(stream, line)) {
        if (line.empty()) {
            if (!stanza.empty()) {
                size_t index = m_allStanzas.size();
                m_allStanzas.push_back(stanza);
                if (isFieldStanza(stanza)) {
                    m_fieldStanzaIndices.push_back(index);
                }

                stanza.clear();
                lastFieldIdx = -1;
            }
            continue;
        }

        auto parsed = parseDeb822Line(line);

        if (parsed.isContinuation && lastFieldIdx != -1) {
            // append to last field value (with newline)
            stanza[lastFieldIdx].value += "\n" + parsed.content;
            // also add continuation line to stanza so it's preserved when saving
            stanza.push_back(parsed);
        } else {
            stanza.push_back(parsed);
            if (parsed.isField())
                lastFieldIdx = stanza.size() - 1;
            else
                lastFieldIdx = -1;
        }
    }

    if (!stanza.empty()) {
        size_t index = m_allStanzas.size();
        m_allStanzas.push_back(stanza);
        if (isFieldStanza(stanza))
            m_fieldStanzaIndices.push_back(index);
    }

    return true;
}

bool Deb822File::loadFromString(const std::string &content)
{
    std::istringstream stream(content);
    return loadFromStream(stream);
}

bool Deb822File::load(const std::string &filename)
{
    std::ifstream file(filename);
    if (!file) {
        m_lastError = "Failed to open file: " + filename;
        return false;
    }
    m_filename = filename;

    return loadFromStream(file);
}

bool Deb822File::save(const std::string &filename)
{
    std::ofstream file(filename);
    if (!file) {
        m_lastError = "Failed to write file: " + filename;
        return false;
    }

    for (size_t i = 0; i < m_allStanzas.size(); ++i) {
        for (const auto &line : m_allStanzas[i])
            file << line.content << "\n";

        if (i + 1 < m_allStanzas.size())
            file << "\n";
    }

    return true;
}

std::string Deb822File::lastError() const
{
    return m_lastError;
}

std::optional<std::string> Deb822File::getFieldValue(
    size_t stanzaIndex,
    const std::string &field,
    std::optional<std::string> defaultValue)
{
    if (stanzaIndex >= m_fieldStanzaIndices.size()) {
        m_lastError = "Stanza index out of range";
        return std::nullopt;
    }

    const Stanza &stanza = m_allStanzas[m_fieldStanzaIndices[stanzaIndex]];
    for (const auto &line : stanza) {
        if (line.key == field) {
            return line.value;
        }
    }

    return defaultValue;
}

bool Deb822File::updateField(size_t stanzaIndex, const std::string &field, const std::string &newValue)
{
    if (stanzaIndex >= m_fieldStanzaIndices.size()) {
        m_lastError = "Stanza index out of range";
        return false;
    }

    Stanza &stanza = m_allStanzas[m_fieldStanzaIndices[stanzaIndex]];
    for (auto it = stanza.begin(); it != stanza.end(); ++it) {
        if (it->key == field) {
            auto next = std::next(it);
            while (next != stanza.end() && next->isContinuation)
                next = stanza.erase(next);

            it->value = newValue;
            std::istringstream valstream(newValue);
            std::string line;
            std::getline(valstream, line);
            it->content = field + ": " + line;

            while (std::getline(valstream, line))
                stanza.insert(next, Line{" " + line, "", "", true});

            return true;
        }
    }

    std::istringstream valstream(newValue);
    std::string line;
    std::getline(valstream, line);
    stanza.push_back(Line{field + ": " + line, field, newValue, false});
    while (std::getline(valstream, line))
        stanza.push_back(Line{" " + line, "", "", true});

    return true;
}

bool Deb822File::deleteField(size_t stanzaIndex, const std::string &key)
{
    if (stanzaIndex >= m_fieldStanzaIndices.size()) {
        m_lastError = "Stanza index out of range";
        return false;
    }

    Stanza &stanza = m_allStanzas[m_fieldStanzaIndices[stanzaIndex]];

    for (auto it = stanza.begin(); it != stanza.end(); ++it) {
        if (it->key == key) {
            // erase the field and its continuation lines
            auto next = std::next(it);
            while (next != stanza.end() && next->isContinuation)
                next = stanza.erase(next);

            stanza.erase(it);

            return true;
        }
    }

    // not found
    m_lastError = "";
    return false;
}

bool Deb822File::deleteStanza(size_t index)
{
    if (index >= m_fieldStanzaIndices.size()) {
        m_lastError = "Stanza index out of range";
        return false;
    }

    size_t rawIndex = m_fieldStanzaIndices[index];
    if (rawIndex >= m_allStanzas.size()) {
        m_lastError = "Internal error: index mismatch";
        return false;
    }

    m_allStanzas.erase(m_allStanzas.begin() + rawIndex);

    // rebuild m_fieldStanzaIndices
    m_fieldStanzaIndices.clear();
    for (size_t i = 0; i < m_allStanzas.size(); ++i) {
        if (isFieldStanza(m_allStanzas[i])) {
            m_fieldStanzaIndices.push_back(i);
        }
    }

    return true;
}

int Deb822File::duplicateStanza(size_t index)
{
    if (index >= m_fieldStanzaIndices.size()) {
        m_lastError = "Stanza index out of range";
        return -1;
    }

    size_t rawIndex = m_fieldStanzaIndices[index];
    if (rawIndex >= m_allStanzas.size()) {
        m_lastError = "Internal error: index mismatch";
        return -1;
    }

    // copy and append
    m_allStanzas.push_back(m_allStanzas[rawIndex]);

    // append new index to fieldStanzaIndices and return it
    size_t newRawIndex = m_allStanzas.size() - 1;
    m_fieldStanzaIndices.push_back(newRawIndex);

    return static_cast<int>(m_fieldStanzaIndices.size() - 1);
}

size_t Deb822File::stanzaCount() const
{
    return m_fieldStanzaIndices.size();
}

std::string Deb822File::toString() const
{
    std::ostringstream out;
    for (size_t i = 0; i < m_allStanzas.size(); ++i) {
        for (const auto &line : m_allStanzas[i])
            out << line.content << "\n";

        if (i + 1 < m_allStanzas.size())
            out << "\n";
    }

    return out.str();
}
