#include "BookImport.hpp"

#include <giomm/file.h>
#include <glibmm/miscutils.h>
#include <glibmm/ustring.h>

#include <tinyxml2.h>
#include <zip.h>            // libzip
#include <poppler.h>        // poppler-glib (C API)
#include <cairo.h>

#include <fstream>
#include <vector>
#include <stdexcept>
#include <algorithm>

// --------------------- utils ---------------------

static std::string to_uri(const std::string& path) {
  return Glib::filename_to_uri(path);
}

std::string ensure_covers_dir() {
  auto base = Glib::build_filename(Glib::get_user_data_dir(), "bibliotheca");
  auto dir  = Glib::build_filename(base, "covers");
  auto f    = Gio::File::create_for_path(dir);
  try {
    auto t = f->query_file_type();
    if (t == Gio::FileType::DIRECTORY) return dir;
    if (t != Gio::FileType::UNKNOWN) throw std::runtime_error("covers path is not a dir");
  } catch (...) { /* not found, will create */ }

  try {
    f->make_directory_with_parents();
  } catch (const Gio::Error& e) {
    if (e.code() != Gio::Error::EXISTS) throw;
  }
  return dir;
}

static std::string lower_ext(const std::string& p) {
  auto dot = p.find_last_of('.');
  std::string ext = (dot == std::string::npos) ? "" : p.substr(dot + 1);
  std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
  return ext;
}

static bool is_pdf(const std::string& p)  { return lower_ext(p) == "pdf"; }
static bool is_epub(const std::string& p) { return lower_ext(p) == "epub"; }

// Save bytes to file
static void write_file(const std::string& out, const std::vector<unsigned char>& bytes) {
  std::ofstream ofs(out, std::ios::binary);
  ofs.write(reinterpret_cast<const char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
}

// --------------------- EPUB (libzip + tinyxml2) ---------------------

// Read a whole file from zip into memory
static std::vector<unsigned char> zip_read_all(zip* z, const std::string& name) {
  zip_stat_t st{};
  if (zip_stat(z, name.c_str(), 0, &st) != 0) throw std::runtime_error("zip_stat failed for " + name);
  zip_file* zf = zip_fopen(z, name.c_str(), 0);
  if (!zf) throw std::runtime_error("zip_fopen failed for " + name);
  std::vector<unsigned char> buf(static_cast<std::size_t>(st.size));
  zip_fread(zf, buf.data(), st.size);
  zip_fclose(zf);
  return buf;
}

static std::string read_text(zip* z, const std::string& name) {
  auto bytes = zip_read_all(z, name);
  return std::string(reinterpret_cast<const char*>(bytes.data()), bytes.size());
}

struct EpubPaths { std::string opfPath; std::string rootDir; };
static EpubPaths epub_find_opf(zip* z) {
  auto xml = read_text(z, "META-INF/container.xml");
  tinyxml2::XMLDocument doc;
  if (doc.Parse(xml.c_str(), xml.size()) != tinyxml2::XML_SUCCESS)
    throw std::runtime_error("bad container.xml");
  auto* rootfiles = doc.FirstChildElement("container")
                       ->FirstChildElement("rootfiles");
  auto* rootfile  = rootfiles ? rootfiles->FirstChildElement("rootfile") : nullptr;
  if (!rootfile) throw std::runtime_error("no rootfile");
  auto* full = rootfile->FindAttribute("full-path");
  if (!full) throw std::runtime_error("rootfile missing full-path");
  std::string opf = full->Value();
  // root dir for relative hrefs
  auto slash = opf.find_last_of('/');
  std::string rootDir = (slash == std::string::npos) ? "" : opf.substr(0, slash + 1);
  return {opf, rootDir};
}

struct EpubMeta { std::string title; std::string author; std::string coverHref; };
static EpubMeta epub_parse_opf(zip* z, const EpubPaths& paths) {
  auto opfXml = read_text(z, paths.opfPath);
  tinyxml2::XMLDocument doc;
  if (doc.Parse(opfXml.c_str(), opfXml.size()) != tinyxml2::XML_SUCCESS)
    throw std::runtime_error("bad OPF");

  auto* pkg = doc.FirstChildElement("package");
  if (!pkg) throw std::runtime_error("opf: no <package>");

  auto* meta = pkg->FirstChildElement("metadata");
  std::string title, author, coverId;

  if (meta) {
    if (auto* t = meta->FirstChildElement("dc:title"))   if (t->GetText()) title  = t->GetText();
    if (auto* a = meta->FirstChildElement("dc:creator")) if (a->GetText()) author = a->GetText();

    // EPUB2 style: <meta name="cover" content="idref"/>
    for (auto* m = meta->FirstChildElement("meta"); m; m = m->NextSiblingElement("meta")) {
      const char* name = m->Attribute("name");
      const char* cont = m->Attribute("content");
      if (name && cont && std::string(name) == "cover") { coverId = cont; break; }
    }
  }

  auto* manifest = pkg->FirstChildElement("manifest");
  std::string coverHref;
  if (manifest) {
    for (auto* item = manifest->FirstChildElement("item"); item; item = item->NextSiblingElement("item")) {
      const char* id  = item->Attribute("id");
      const char* href= item->Attribute("href");
      const char* props = item->Attribute("properties");
      if (!href) continue;

      // EPUB3 cover item
      if (props && std::string(props).find("cover-image") != std::string::npos) {
        coverHref = href; break;
      }
      // EPUB2 by id from <meta name="cover">
      if (!coverHref.empty()) continue;
      if (id && coverId == id) coverHref = href;
    }
  }

  // Normalize path
  if (!coverHref.empty() && !paths.rootDir.empty())
    coverHref = paths.rootDir + coverHref;

  return {title, author, coverHref};
}

static std::optional<ImportResult> import_epub(const std::string& filePath,
                                               const std::string& bookIdHex,
                                               const std::string& coversDir) {
  int err = 0;
  zip* z = zip_open(filePath.c_str(), ZIP_RDONLY, &err);
  if (!z) throw std::runtime_error("cannot open epub as zip");

  ImportResult result;
  try {
    auto paths = epub_find_opf(z);
    auto meta  = epub_parse_opf(z, paths);
    result.title  = meta.title;
    result.author = meta.author;

    if (!meta.coverHref.empty()) {
      auto bytes = zip_read_all(z, meta.coverHref);
      std::string ext = lower_ext(meta.coverHref);
      // Always write PNG (gdk can load many types, but normalize is nice). If already png, write as-is.
      // For simplicity we just write raw bytes; if it's jpg/svg/png it will be fine for Texture::create_from_filename.
      std::string out = Glib::build_filename(coversDir, bookIdHex + "." + (ext.empty() ? "png" : ext));
      write_file(out, bytes);
      result.coverPngPath = out;
    }
  } catch (...) {
    zip_close(z);
    throw;
  }
  zip_close(z);
  return result;
}

// --------------------- PDF (poppler-glib + cairo) ---------------------

static std::optional<ImportResult> import_pdf(const std::string& filePath,
                                              const std::string& bookIdHex,
                                              const std::string& coversDir) {
  GError* gerr = nullptr;
  // Poppler expects a URI
  auto* doc = poppler_document_new_from_file(to_uri(filePath).c_str(), /*password*/nullptr, &gerr);
  if (!doc) {
    std::string message = "poppler open failed";
    if (gerr) {
      message = gerr->message ? gerr->message : message;
      g_error_free(gerr);
    }
    throw std::runtime_error(std::move(message));
  }

  ImportResult result;
  // Metadata
  {
    char* t = poppler_document_get_title(doc);
    char* a = poppler_document_get_author(doc);
    if (t) { result.title  = t; g_free(t); }
    if (a) { result.author = a; g_free(a); }
  }

  // Render page 0 to a PNG
  if (poppler_document_get_n_pages(doc) > 0) {
    PopplerPage* page = poppler_document_get_page(doc, 0);
    double wpt=0, hpt=0; poppler_page_get_size(page, &wpt, &hpt);

    // scale to 1000px height (nice thumbnails); keep aspect
    const double target_h = 1000.0;
    const double scale = (hpt > 0) ? (target_h / hpt) : 1.0;
    int width  = std::max(1, static_cast<int>(wpt * scale));
    int height = std::max(1, static_cast<int>(hpt * scale));

    cairo_surface_t* surf = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
    cairo_t* cr = cairo_create(surf);
    cairo_scale(cr, scale, scale);
    poppler_page_render(page, cr);
    cairo_surface_flush(surf);

    std::string out = Glib::build_filename(coversDir, bookIdHex + ".png");
    cairo_status_t st = cairo_surface_write_to_png(surf, out.c_str());
    if (st == CAIRO_STATUS_SUCCESS) {
      result.coverPngPath = out;
    }

    cairo_destroy(cr);
    cairo_surface_destroy(surf);
    g_object_unref(page);
  }

  g_object_unref(doc);
  return result;
}

// --------------------- main dispatcher ---------------------

std::optional<ImportResult> import_book_assets(const std::string& filePath,
                                               const std::string& bookIdHex) {
  const auto coversDir = ensure_covers_dir();

  if (is_epub(filePath)) return import_epub(filePath, bookIdHex, coversDir);
  if (is_pdf(filePath))  return import_pdf(filePath,  bookIdHex, coversDir);

  // Unknown type — return empty result so callers can still add the file.
  return ImportResult{};
}
