#include "origincar_base/log.hpp"

#include <cerrno>
#include <cctype>
#include <cmath>
#include <ctime>
#include <iomanip>
#include <sstream>
#include <sys/stat.h>
#include <sys/types.h>

namespace origincar_base_logging
{
namespace
{
std::string sanitizeNodeName(const std::string &node_name)
{
  std::string result;
  for (char ch : node_name)
  {
    const unsigned char value = static_cast<unsigned char>(ch);
    if (std::isalnum(value) || ch == '_' || ch == '-')
    {
      result.push_back(ch);
    }
    else if (!result.empty() && result.back() != '_')
    {
      result.push_back('_');
    }
  }
  return result.empty() ? "node" : result;
}

bool makeDirectory(const std::string &path)
{
  if (path.empty())
  {
    return false;
  }
  if (::mkdir(path.c_str(), 0755) == 0 || errno == EEXIST)
  {
    return true;
  }
  return false;
}

bool makeDirectories(const std::string &path)
{
  if (path.empty())
  {
    return false;
  }

  std::string current;
  for (std::size_t index = 0; index < path.size(); ++index)
  {
    current.push_back(path[index]);
    if ((path[index] == '/' && current.size() > 1) || index + 1 == path.size())
    {
      if (current.size() > 1 && current.back() == '/')
      {
        current.pop_back();
      }
      if (!current.empty() && !makeDirectory(current))
      {
        return false;
      }
      if (index + 1 < path.size() && path[index] == '/')
      {
        current.push_back('/');
      }
    }
  }
  return true;
}

std::string formatSystemTime()
{
  const std::time_t now = std::time(nullptr);
  std::tm local_time;
  localtime_r(&now, &local_time);
  std::ostringstream stream;
  stream << std::put_time(&local_time, "%Y-%m-%d %H:%M:%S");
  return stream.str();
}

std::string formatFileTimestamp()
{
  const std::time_t now = std::time(nullptr);
  std::tm local_time;
  localtime_r(&now, &local_time);
  std::ostringstream stream;
  stream << std::put_time(&local_time, "%Y%m%d_%H%M%S");
  return stream.str();
}

double elapsedMs(ScanOdomTimingLogger::TimePoint start, ScanOdomTimingLogger::TimePoint end)
{
  return std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(end - start).count();
}
}  // namespace

ScanOdomTimingLogger::ScanOdomTimingLogger(
    const std::string &node_name,
    const std::string &base_log_dir,
    TimePoint start_time)
    : last_summary_time_(start_time),
      log_path_(makeLogPath(node_name, base_log_dir)),
      log_file_(log_path_.c_str(), std::ios::out | std::ios::app)
{
}

uint64_t ScanOdomTimingLogger::startFrame(TimePoint now)
{
  const uint64_t frame_id = next_frame_id_++;
  active_frames_[frame_id] = now;
  return frame_id;
}

bool ScanOdomTimingLogger::finishFrame(uint64_t frame_id, TimePoint now)
{
  const auto frame = active_frames_.find(frame_id);
  if (frame == active_frames_.end())
  {
    return false;
  }

  const double duration_ms = elapsedMs(frame->second, now);
  active_frames_.erase(frame);
  stats_.count += 1;
  stats_.total_ms += duration_ms;
  stats_.last_ms = duration_ms;
  stats_.max_ms = std::max(stats_.max_ms, duration_ms);
  stats_.min_ms = std::min(stats_.min_ms, duration_ms);
  writeFinishedFrame(frame_id, duration_ms);
  return true;
}

bool ScanOdomTimingLogger::cancelFrame(uint64_t frame_id, const std::string &reason, TimePoint now)
{
  const auto frame = active_frames_.find(frame_id);
  if (frame == active_frames_.end())
  {
    return false;
  }

  const double elapsed_ms = elapsedMs(frame->second, now);
  active_frames_.erase(frame);
  writeCanceledFrame(frame_id, elapsed_ms, reason);
  return true;
}

bool ScanOdomTimingLogger::makeSummaryLineIfDue(TimePoint now, std::string *line)
{
  if (now - last_summary_time_ < std::chrono::seconds(1))
  {
    return false;
  }
  last_summary_time_ = now;
  if (line)
  {
    *line = formatStatsLine("scan_to_odom timing");
  }
  return true;
}

void ScanOdomTimingLogger::setPrintToTerminal(bool enabled)
{
  print_to_terminal_ = enabled;
}

bool ScanOdomTimingLogger::printToTerminal() const
{
  return print_to_terminal_;
}

const std::string &ScanOdomTimingLogger::logPath() const
{
  return log_path_;
}

bool ScanOdomTimingLogger::isOpen() const
{
  return log_file_.is_open();
}

std::string ScanOdomTimingLogger::makeLogPath(
    const std::string &node_name,
    const std::string &base_log_dir) const
{
  const std::string node_dir = base_log_dir + "/" + sanitizeNodeName(node_name);
  makeDirectories(node_dir);
  return node_dir + "/" + formatFileTimestamp() + ".log";
}

void ScanOdomTimingLogger::writeFinishedFrame(uint64_t frame_id, double duration_ms)
{
  if (!log_file_.is_open())
  {
    return;
  }
  log_file_ << formatSystemTime()
            << " frame_id=" << frame_id
            << " status=finished"
            << std::fixed << std::setprecision(3)
            << " duration_ms=" << duration_ms
            << " " << formatStatsLine("stats")
            << '\n';
  log_file_.flush();
}

void ScanOdomTimingLogger::writeCanceledFrame(uint64_t frame_id, double elapsed_ms, const std::string &reason)
{
  if (!log_file_.is_open())
  {
    return;
  }
  log_file_ << formatSystemTime()
            << " frame_id=" << frame_id
            << " status=canceled"
            << " reason=" << reason
            << std::fixed << std::setprecision(3)
            << " elapsed_ms=" << elapsed_ms
            << '\n';
  log_file_.flush();
}

std::string ScanOdomTimingLogger::formatStatsLine(const std::string &prefix) const
{
  const double average_ms = stats_.count == 0 ? 0.0 : stats_.total_ms / static_cast<double>(stats_.count);
  const double min_ms = stats_.count == 0 ? 0.0 : stats_.min_ms;
  std::ostringstream stream;
  stream << prefix
         << " frames=" << stats_.count
         << std::fixed << std::setprecision(3)
         << " last_ms=" << stats_.last_ms
         << " avg_ms=" << average_ms
         << " max_ms=" << stats_.max_ms
         << " min_ms=" << min_ms;
  return stream.str();
}
}  // namespace origincar_base_logging
