#include "core/project_journal.hpp"

#include "core/json.hpp"

#include <fstream>
#include <sstream>
#include <string>

namespace smartcut::core {
namespace {

constexpr std::size_t maximum_journal_line = 16U * 1024U * 1024U;

template <typename Value>
std::string csv_values(const std::vector<Value>& values) {
  std::ostringstream output;
  for (std::size_t index = 0; index < values.size(); ++index) {
    if (index) output << ',';
    output << values[index];
  }
  return output.str();
}

template <typename Value>
std::optional<std::vector<Value>> parse_values(std::string_view text) {
  std::vector<Value> result;
  std::size_t cursor = 0;
  while (cursor <= text.size()) {
    const auto separator = text.find(',', cursor);
    const auto token = text.substr(cursor, separator == std::string_view::npos
                                              ? text.size() - cursor
                                              : separator - cursor);
    if (!token.empty()) {
      const auto parsed = parse_unsigned<Value>(token);
      if (!parsed) return std::nullopt;
      result.push_back(*parsed);
    }
    if (separator == std::string_view::npos) break;
    cursor = separator + 1;
  }
  return result;
}

std::optional<std::vector<std::pair<std::uint64_t, std::uint64_t>>> parse_ranges(
    std::string_view text, std::uint64_t frame_count) {
  std::vector<std::pair<std::uint64_t, std::uint64_t>> result;
  std::size_t cursor = 0;
  while (cursor <= text.size()) {
    const auto comma = text.find(',', cursor);
    const auto token = text.substr(cursor, comma == std::string_view::npos
                                              ? text.size() - cursor
                                              : comma - cursor);
    if (!token.empty()) {
      const auto separator = token.find(':');
      if (separator == std::string_view::npos || token.find(':', separator + 1) != std::string_view::npos)
        return std::nullopt;
      const auto first = parse_unsigned<std::uint64_t>(token.substr(0, separator));
      const auto end = parse_unsigned<std::uint64_t>(token.substr(separator + 1));
      if (!first || !end || *first >= *end || *end > frame_count) return std::nullopt;
      result.emplace_back(*first, *end);
    }
    if (comma == std::string_view::npos) break;
    cursor = comma + 1;
  }
  return result;
}

bool valid_snapshot(const EditSnapshot& snapshot) {
  if (snapshot.source_uuid.empty() || snapshot.frame_count == 0) return false;
  for (const auto& [first, end] : snapshot.cuts) {
    if (first >= end || end > snapshot.frame_count) return false;
  }
  for (const auto chapter : snapshot.chapter_markers) {
    if (chapter >= snapshot.frame_count) return false;
  }
  return true;
}

std::optional<std::string> required_field(std::string_view line, std::string_view key) {
  const auto result = json_field(line, key);
  if (!result || result->empty()) return std::nullopt;
  return result;
}

}  // namespace

bool ProjectJournal::append_add_segment(const std::filesystem::path& path,
                                        const KeptSegment& segment) {
  std::ofstream output(path, std::ios::app);
  if (!output) return false;
  output << "{\"op\":\"add_segment\",\"source_uuid\":\"" << json_escape(segment.source_uuid)
         << "\",\"video_stream\":" << segment.video_stream
         << ",\"first_display_frame\":" << segment.first_display_frame
         << ",\"end_display_frame_exclusive\":" << segment.end_display_frame_exclusive << "}\n";
  return static_cast<bool>(output);
}

bool ProjectJournal::append_export_evidence(const std::filesystem::path& path,
                                            const ExportEvidence& evidence) {
  std::ofstream output(path, std::ios::app);
  if (!output) return false;
  output << "{\"op\":\"export_evidence\",\"source_uuid\":\"" << json_escape(evidence.source_uuid)
         << "\",\"output_uuid\":\"" << json_escape(evidence.output_uuid)
         << "\",\"first_frame\":" << evidence.first_frame << ",\"end_frame\":" << evidence.end_frame
         << ",\"status\":\"" << json_escape(evidence.status) << "\"}\n";
  return static_cast<bool>(output);
}

bool ProjectJournal::append_audio_validation(const std::filesystem::path& path,
                                             const std::string& status) {
  std::ofstream output(path, std::ios::app);
  if (!output) return false;
  output << "{\"op\":\"audio_validation\",\"status\":\"" << json_escape(status) << "\"}\n";
  return static_cast<bool>(output);
}

bool ProjectJournal::append_edit_snapshot(const std::filesystem::path& path,
                                          const EditSnapshot& snapshot) {
  if (!valid_snapshot(snapshot)) return false;
  std::ofstream output(path, std::ios::app);
  if (!output) return false;
  std::ostringstream cuts;
  for (std::size_t index = 0; index < snapshot.cuts.size(); ++index) {
    if (index) cuts << ',';
    cuts << snapshot.cuts[index].first << ':' << snapshot.cuts[index].second;
  }
  output << "{\"op\":\"edit_snapshot\",\"source_uuid\":\"" << json_escape(snapshot.source_uuid)
         << "\",\"frame_count\":" << snapshot.frame_count << ",\"cuts\":\"" << cuts.str()
         << "\",\"streams\":\"" << csv_values(snapshot.selected_streams)
         << "\",\"chapters\":\"" << csv_values(snapshot.chapter_markers)
         << "\",\"scene_mode\":" << (snapshot.scene_mode ? "true" : "false")
         << ",\"audio_offset_ms\":" << snapshot.audio_offset_ms << "}\n";
  return static_cast<bool>(output);
}

std::optional<EditSnapshot> ProjectJournal::latest_edit_snapshot(const std::filesystem::path& path) {
  std::ifstream input(path);
  if (!input) return std::nullopt;
  std::string line;
  std::optional<EditSnapshot> latest;
  while (std::getline(input, line)) {
    if (line.size() > maximum_journal_line ||
        json_field(line, "op") != std::optional<std::string>{"edit_snapshot"}) continue;
    const auto source_uuid = required_field(line, "source_uuid");
    const auto frame_count_text = required_field(line, "frame_count");
    const auto cuts_text = json_field(line, "cuts");
    const auto streams_text = json_field(line, "streams");
    const auto chapters_text = json_field(line, "chapters");
    const auto scene_mode_text = required_field(line, "scene_mode");
    if (!source_uuid || !frame_count_text || !cuts_text || !streams_text || !chapters_text || !scene_mode_text)
      continue;
    const auto frame_count = parse_unsigned<std::uint64_t>(*frame_count_text);
    if (!frame_count || *frame_count == 0) continue;
    const auto cuts = parse_ranges(*cuts_text, *frame_count);
    const auto streams = parse_values<std::size_t>(*streams_text);
    const auto chapters = parse_values<std::uint64_t>(*chapters_text);
    if (!cuts || !streams || !chapters) continue;
    EditSnapshot candidate;
    candidate.source_uuid = *source_uuid;
    candidate.frame_count = *frame_count;
    candidate.cuts = *cuts;
    candidate.selected_streams = *streams;
    candidate.chapter_markers = *chapters;
    candidate.scene_mode = *scene_mode_text == "true";
    if (*scene_mode_text != "true" && *scene_mode_text != "false") continue;
    if (const auto offset = json_field(line, "audio_offset_ms"); offset && !offset->empty()) {
      const auto parsed = parse_signed<std::int64_t>(*offset);
      if (!parsed) continue;
      candidate.audio_offset_ms = *parsed;
    }
    if (valid_snapshot(candidate)) latest = std::move(candidate);
  }
  return latest;
}

bool ProjectJournal::recover(const std::filesystem::path& path, Project& project) {
  std::ifstream input(path);
  if (!input) return false;
  std::string line;
  while (std::getline(input, line)) {
    if (line.size() > maximum_journal_line) return false;
    const auto op = json_field(line, "op");
    if (!op) {
      // A crash can leave the final append-only journal record incomplete.
      // Ignore only that unterminated tail; malformed newline-terminated
      // records remain a hard failure so corrupt or hostile input is visible.
      if (input.eof()) break;
      return false;
    }
    if (*op == "add_segment") {
      const auto source = required_field(line, "source_uuid");
      const auto stream = json_field(line, "video_stream");
      const auto first = json_field(line, "first_display_frame");
      const auto end = json_field(line, "end_display_frame_exclusive");
      if (!source || !stream || !first || !end) return false;
      const auto parsed_stream = parse_unsigned<std::size_t>(*stream);
      const auto parsed_first = parse_unsigned<std::uint64_t>(*first);
      const auto parsed_end = parse_unsigned<std::uint64_t>(*end);
      if (!parsed_stream || !parsed_first || !parsed_end || *parsed_first >= *parsed_end) return false;
      project.add_segment({*source, *parsed_stream, *parsed_first, *parsed_end});
    } else if (*op == "export_evidence") {
      const auto source = required_field(line, "source_uuid");
      const auto output = required_field(line, "output_uuid");
      const auto first = json_field(line, "first_frame");
      const auto end = json_field(line, "end_frame");
      const auto status = required_field(line, "status");
      if (!source || !output || !first || !end || !status) return false;
      const auto parsed_first = parse_unsigned<std::uint64_t>(*first);
      const auto parsed_end = parse_unsigned<std::uint64_t>(*end);
      if (!parsed_first || !parsed_end || *parsed_first > *parsed_end) return false;
      project.add_export_evidence({*source, *output, *parsed_first, *parsed_end, *status});
    } else if (*op == "audio_validation") {
      const auto status = required_field(line, "status");
      if (!status) return false;
      project.set_audio_validation_status(*status);
    } else if (*op != "edit_snapshot") {
      // Journals are append-only and may contain operations introduced by a
      // newer SmartCut64 version. Preserve the previous forward-compatible
      // behaviour while parsing every operation we do understand strictly.
      continue;
    }
  }
  return project.valid();
}

}  // namespace smartcut::core
