#include "calendar.h"

#include <curl/curl.h>
#include <fstream>
#include <iostream>
#include <iomanip>
#include <sstream>
#include <vector>

static size_t WriteCB(void *contents, size_t size, size_t nmemb, void *userp) {
  size_t total = size * nmemb;
  std::string *s = reinterpret_cast<std::string *>(userp);
  s->append(reinterpret_cast<const char *>(contents), total);
  return total;
}

static bool EndsWith(const std::string &s, const std::string &suffix) {
  if (s.size() < suffix.size()) return false;
  return s.compare(s.size() - suffix.size(), suffix.size(), suffix) == 0;
}

static std::vector<std::string> UnfoldLines(const std::string &ics) {
  std::vector<std::string> out;
  std::istringstream iss(ics);
  std::string line;
  while (std::getline(iss, line)) {
    if (!out.empty() && !line.empty() && (line[0] == ' ' || line[0] == '\t')) {
      // continuation line, append without the leading whitespace
      out.back() += line.substr(1);
    } else {
      // strip trailing CR if present
      if (!line.empty() && line.back() == '\r') line.pop_back();
      out.push_back(line);
    }
  }
  return out;
}

#if defined(_WIN32)
static std::time_t timegm_portable(std::tm *tm) { return _mkgmtime(tm); }
#else
static std::time_t timegm_portable(std::tm *tm) { return timegm(tm); }
#endif

static bool ParseICalDateTime(const std::string &value, std::time_t *out_t, bool *out_all_day) {
  // Formats we handle:
  // 1) YYYYMMDDTHHMMSSZ (UTC)
  // 2) YYYYMMDDTHHMMSS  (local)
  // 3) YYYYMMDD         (all-day date)
  if (!out_t) return false;

  std::string v = value;
  if (v.empty()) return false;

  if (v.size() == 8 && std::isdigit(v[0])) {
    // all-day
    std::tm tm{};
    if (strptime(v.c_str(), "%Y%m%d", &tm) == nullptr) return false;
    tm.tm_hour = 0; tm.tm_min = 0; tm.tm_sec = 0;
    *out_t = std::mktime(&tm);  // local midnight
    if (out_all_day) *out_all_day = true;
    return true;
  }

  bool utc = false;
  if (EndsWith(v, "Z")) {
    utc = true;
    v = v.substr(0, v.size() - 1);
  }

  std::tm tm{};
  if (strptime(v.c_str(), "%Y%m%dT%H%M%S", &tm) == nullptr) {
    // Some feeds omit seconds: YYYYMMDDTHHMM
    tm = std::tm{};
    if (strptime(v.c_str(), "%Y%m%dT%H%M", &tm) == nullptr) return false;
  }

  if (utc) {
    *out_t = timegm_portable(&tm);
  } else {
    *out_t = std::mktime(&tm);
  }

  if (out_all_day) *out_all_day = false;
  return true;
}

static std::string TwoDigit(int v) {
  char b[8];
  snprintf(b, sizeof(b), "%02d", v);
  return b;
}

static std::string FormatTime12(std::time_t t) {
  std::tm lt{};
  localtime_r(&t, &lt);
  int h = lt.tm_hour;
  int m = lt.tm_min;
  bool pm = h >= 12;
  int h12 = h % 12;
  if (h12 == 0) h12 = 12;

  std::string s = std::to_string(h12);
  s += ":";
  s += TwoDigit(m);
  s += pm ? "p" : "a";
  return s;
}

static int DaysBetweenLocalDate(std::time_t a, std::time_t b) {
  // Normalize to local midnight for both and diff in days.
  std::tm ta{}, tb{};
  localtime_r(&a, &ta);
  localtime_r(&b, &tb);
  ta.tm_hour = ta.tm_min = ta.tm_sec = 0;
  tb.tm_hour = tb.tm_min = tb.tm_sec = 0;
  std::time_t aa = std::mktime(&ta);
  std::time_t bb = std::mktime(&tb);
  double diff = std::difftime(aa, bb);
  return (int)(diff / 86400.0);
}

static std::string FormatWhen(std::time_t start, std::time_t end, bool all_day) {
  std::time_t now = std::time(nullptr);
  int day_delta = DaysBetweenLocalDate(start, now);

  std::tm lt{};
  localtime_r(&start, &lt);

  std::string day;
  if (day_delta == 0) day = "Today";
  else if (day_delta == 1) day = "Tomorrow";
  else {
    // Mon 2/23
    static const char *wday[] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"};
    day = std::string(wday[lt.tm_wday]) + " " +
          std::to_string(lt.tm_mon + 1) + "/" + std::to_string(lt.tm_mday);
  }

  if (all_day) return day + " All day";

  std::string s = day + " " + FormatTime12(start);
  if (end != 0 && end != start) {
    s += "\xe2\x80\x93"; // en dash
    s += FormatTime12(end);
  }
  return s;
}

bool FetchCalendarICS(const std::string &url, CalendarEvent *out) {
  if (!out) return false;
  *out = CalendarEvent{};

  CURL *curl = curl_easy_init();
  if (!curl) return false;

  std::string body;
  curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
  curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCB);
  curl_easy_setopt(curl, CURLOPT_WRITEDATA, &body);

  // Harden for appliance behavior.
  curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 3L);
  curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L);
  curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
  curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "");
  curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
  curl_easy_setopt(curl, CURLOPT_PROXY, "");

  struct curl_slist *headers = nullptr;
  headers = curl_slist_append(headers, "User-Agent: infocube/1.0");
  curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);

  char errbuf[CURL_ERROR_SIZE];
  errbuf[0] = 0;
  curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf);

  CURLcode res = curl_easy_perform(curl);

  long http_code = 0;
  curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

  if (res != CURLE_OK) {
    std::cerr << "Calendar curl error: " << curl_easy_strerror(res);
    if (errbuf[0]) std::cerr << " detail=" << errbuf;
    std::cerr << " http=" << http_code << "\n";
    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);
    return false;
  }

  if (http_code != 200 || body.size() < 64) {
    std::cerr << "Calendar HTTP error: " << http_code << " bytes=" << body.size() << "\n";
    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);
    return false;
  }

  auto lines = UnfoldLines(body);

  // Parse events
  struct TmpEvent {
    std::string summary;
    std::time_t start = 0;
    std::time_t end = 0;
    bool all_day = false;
    bool have_start = false;
  };

  std::vector<TmpEvent> events;
  bool in_event = false;
  TmpEvent cur;

  for (const auto &ln : lines) {
    if (ln == "BEGIN:VEVENT") {
      in_event = true;
      cur = TmpEvent{};
      continue;
    }
    if (ln == "END:VEVENT") {
      if (in_event && cur.have_start) {
        // If end missing for all-day, assume +24h
        if (cur.end == 0 && cur.all_day) cur.end = cur.start + 86400;
        events.push_back(cur);
      }
      in_event = false;
      continue;
    }
    if (!in_event) continue;

    auto pos = ln.find(':');
    if (pos == std::string::npos) continue;
    std::string key = ln.substr(0, pos);
    std::string val = ln.substr(pos + 1);

    if (key.rfind("SUMMARY", 0) == 0) {
      cur.summary = val;
    } else if (key.rfind("DTSTART", 0) == 0) {
      bool all_day = false;
      std::time_t t = 0;
      if (ParseICalDateTime(val, &t, &all_day)) {
        cur.start = t;
        cur.all_day = all_day || (key.find("VALUE=DATE") != std::string::npos);
        cur.have_start = true;
      }
    } else if (key.rfind("DTEND", 0) == 0) {
      bool all_day = false;
      std::time_t t = 0;
      if (ParseICalDateTime(val, &t, &all_day)) {
        cur.end = t;
      }
    }
  }

  std::time_t now = std::time(nullptr);

  // Choose best event: prefer currently active, else earliest upcoming.
  bool found = false;
  TmpEvent best;
  for (const auto &e : events) {
    std::time_t end = e.end ? e.end : e.start;
    bool active = (e.start <= now && now < end);
    bool upcoming = (e.start >= now);

    if (!active && !upcoming) continue;

    if (!found) {
      best = e; found = true;
      continue;
    }

    // Active beats upcoming
    bool best_active = (best.start <= now && now < (best.end ? best.end : best.start));
    if (active && !best_active) { best = e; continue; }
    if (active && best_active) {
      // choose the one ending soonest
      std::time_t best_end = best.end ? best.end : best.start;
      if (end < best_end) best = e;
      continue;
    }

    if (!best_active && upcoming) {
      if (e.start < best.start) best = e;
    }
  }

  if (!found) {
    out->title = "No upcoming events";
    out->ok = true;
    out->fetched_at = std::time(nullptr);
    out->when = "";
  } else {
    out->title = best.summary.empty() ? "Untitled" : best.summary;
    out->start = best.start;
    out->end = best.end;
    out->all_day = best.all_day;
    out->when = FormatWhen(out->start, out->end, out->all_day);
    out->ok = true;
    out->fetched_at = std::time(nullptr);
  }

  curl_slist_free_all(headers);
  curl_easy_cleanup(curl);
  return true;
}

bool SaveCalendarCache(const std::string &path, const CalendarEvent &ev) {
  std::ofstream f(path);
  if (!f.is_open()) return false;
  f << "{\n";
  f << "  \"ok\": " << (ev.ok ? "true" : "false") << ",\n";
  f << "  \"title\": " << std::quoted(ev.title) << ",\n";
  f << "  \"start\": " << (long long)ev.start << ",\n";
  f << "  \"end\": " << (long long)ev.end << ",\n";
  f << "  \"all_day\": " << (ev.all_day ? "true" : "false") << ",\n";
  f << "  \"when\": " << std::quoted(ev.when) << ",\n";
  f << "  \"fetched_at\": " << (long long)ev.fetched_at << "\n";
  f << "}\n";
  return true;
}

static bool ExtractQuoted(const std::string &line, std::string *out) {
  auto first = line.find('"');
  if (first == std::string::npos) return false;
  auto second = line.find('"', first + 1);
  if (second == std::string::npos) return false;
  *out = line.substr(first + 1, second - first - 1);
  return true;
}

bool LoadCalendarCache(const std::string &path, CalendarEvent *out) {
  if (!out) return false;
  std::ifstream f(path);
  if (!f.is_open()) return false;

  CalendarEvent ev;
  std::string line;
  while (std::getline(f, line)) {
    if (line.find("\"ok\"") != std::string::npos) {
      ev.ok = (line.find("true") != std::string::npos);
    } else if (line.find("\"title\"") != std::string::npos) {
      std::string v;
      if (ExtractQuoted(line, &v)) ev.title = v;
    } else if (line.find("\"when\"") != std::string::npos) {
      std::string v;
      if (ExtractQuoted(line, &v)) ev.when = v;
    } else if (line.find("\"start\"") != std::string::npos) {
      long long n = 0;
      if (sscanf(line.c_str(), " \"start\": %lld", &n) == 1) ev.start = (std::time_t)n;
    } else if (line.find("\"end\"") != std::string::npos) {
      long long n = 0;
      if (sscanf(line.c_str(), " \"end\": %lld", &n) == 1) ev.end = (std::time_t)n;
    } else if (line.find("\"all_day\"") != std::string::npos) {
      ev.all_day = (line.find("true") != std::string::npos);
    } else if (line.find("\"fetched_at\"") != std::string::npos) {
      long long n = 0;
      if (sscanf(line.c_str(), " \"fetched_at\": %lld", &n) == 1) ev.fetched_at = (std::time_t)n;
    }
  }

  if (ev.title.empty() && !ev.ok) return false;
  *out = ev;
  return true;
}
