//
// ZoneMinder Streaming Server, $Date$, $Revision$
// Copyright (C) 2001-2008 Philip Coombes
//
// 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; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//

#include "zm.h"
#include "zm_db.h"
#include "zm_user.h"
#include "zm_signal.h"
#include "zm_monitorstream.h"
#include "zm_eventstream.h"
#include "zm_fifo_stream.h"
#include <iomanip>
#include <sstream>
#include <string>
#include <unistd.h>

bool ValidateAccess(User *user, int mon_id) {
  bool allowed = true;

  if (mon_id > 0) {
    if (user->getStream() < User::PERM_VIEW) {
      allowed = false;
      Warning("Insufficient privileges for request user %d %s for monitor %d, user does not have permission to stream live view.",
              user->Id(), user->getUsername(), mon_id);
    } else if (!user->canAccess(mon_id)) {
      allowed = false;
      Warning("Insufficient privileges for request user %d %s for monitor %d, user does not have permission view this monitor.",
              user->Id(), user->getUsername(), mon_id);
    }
  } else if (user->getEvents() < User::PERM_VIEW) {
    allowed = false;
    Warning("Insufficient privileges for request user %d %s, user does not have permission view events.",
            user->Id(), user->getUsername());
  }
  return allowed;
}

int main(int argc, const char *argv[], char **envp) {
  self = argv[0];

  srand(getpid() * time(nullptr));

  enum { ZMS_UNKNOWN, ZMS_MONITOR, ZMS_EVENT, ZMS_FIFO } source = ZMS_UNKNOWN;
  enum { ZMS_JPEG, ZMS_MPEG, ZMS_RAW, ZMS_ZIP, ZMS_SINGLE } mode = ZMS_JPEG;
  char format[32] = "";
  int monitor_id = 0;
  SystemTimePoint event_time = std::chrono::system_clock::time_point::min();
  uint64_t event_id = 0;
  unsigned int frame_id = 1;
  int frames_to_send = -1;
  unsigned int scale = 100;
  unsigned int rate = 100;
  double maxfps = 0.0;
  unsigned int bitrate = 100000;
  unsigned int ttl = 0;
  bool  analysis_frames = false;
  EventStream::StreamMode replay = EventStream::MODE_NONE;
  std::string username;
  std::string password;
  char auth[64] = "";
  std::string jwt_token_str = "";
  unsigned int connkey = 0;
  unsigned int playback_buffer = 0;

  bool nph = false;
  const char *basename = strrchr(argv[0], '/');
  if ( basename )  // if we found a / lets skip past it
    basename++;
  else  // argv[0] might not contain the full path, but just the script name
    basename = argv[0];
  const char *nph_prefix = "nph-";
  if ( basename && !strncmp(basename, nph_prefix, strlen(nph_prefix)) ) {
    nph = true;
  }

  char log_id_string[32] = "zms";
  logInit(log_id_string);
  zmLoadStaticConfig();
  zmDbConnect();
  zmLoadDBConfig();
  logInit(log_id_string);

  for (char **env = envp; *env != 0; env++) {
    char *thisEnv = *env;
    Debug(1, "env: %s", thisEnv);
  }

  const char *query = getenv("QUERY_STRING");
  if ( query == nullptr ) {
    Fatal("No query string.");
    return exit_zm(0);
  }  // end if query

  Debug(1, "Query: %s", query);

  char *q_ptr = (char *)query;
  char *parms[16];  // Shouldn't be more than this
  int parm_no = 0;
  while ( (parm_no < 16) && (parms[parm_no] = strtok(q_ptr, "&")) ) {
    parm_no++;
    q_ptr = nullptr;
  }

  for ( int p = 0; p < parm_no; p++ ) {
    char *name = strtok(parms[p], "=");
    char const *value = strtok(nullptr, "=");
    if ( !value )
      value = "";
    if ( !strcmp(name, "analysis") ) {
      if ( !strcmp(value, "true") ) {
        analysis_frames = true;
      } else {
        analysis_frames = (atoi(value) == 1);
      }
      Debug(1, "Viewing analysis frames");
    } else if ( !strcmp(name, "source") ) {
      if ( !strcmp(value, "event") ) {
        source = ZMS_EVENT;
      } else if ( !strcmp(value, "fifo") ) {
        source = ZMS_FIFO;
      } else {
        source = ZMS_MONITOR;
      }
    } else if ( !strcmp(name, "mode") ) {
      if (!strcmp(value, "jpeg")) {
        mode = ZMS_JPEG;
      } else if (!strcmp(value, "single")) {
        mode = ZMS_SINGLE;
      } else if (!strcmp(value, "raw")) {
        mode = ZMS_RAW;
      } else if (!strcmp(value, "zip")) {
        mode = ZMS_ZIP;
      } else if (!strcmp(value, "mpeg")) {
        mode = ZMS_MPEG;
      } else if (!strcmp(value, "paused")) {
        mode = ZMS_JPEG;
      } else {
        Error("Unsupported value for mode");
      }
    } else if ( !strcmp(name, "format") ) {
      strncpy(format, value, sizeof(format)-1);
    } else if ( !strcmp(name, "monitor") ) {
      monitor_id = atoi(value);
      if ( source == ZMS_UNKNOWN )
        source = ZMS_MONITOR;
    } else if ( !strcmp(name, "datetime") ) {
      std::tm tm = {};
      std::stringstream ss(value);
      ss >> std::get_time(&tm, "%Y-%m-%d %H:%M:%S");
      event_time = std::chrono::system_clock::from_time_t(std::mktime(&tm));
      //Debug(1, "Setting event_time to %s", SystemTimePointToString(event_time).c_str());
    } else if ( !strcmp(name, "time") ) {
      double double_time = atof(value);
      std::chrono::duration<double> event_time_seconds(double_time);
      auto system_duration = std::chrono::duration_cast<std::chrono::system_clock::duration>(event_time_seconds);
	    event_time = SystemTimePoint{system_duration};
      //Debug(1, "Setting event_time to %s from %s %.2f %lf",
          //SystemTimePointToString(event_time).c_str(), value, double_time, event_time_seconds.count());
    } else if ( !strcmp(name, "event") ) {
      event_id = strtoull(value, nullptr, 10);
      source = ZMS_EVENT;
    } else if ( !strcmp(name, "frame") ) {
      frame_id = strtoull(value, nullptr, 10);
      source = ZMS_EVENT;
    } else if ( !strcmp(name, "frames") ) {
      frames_to_send = strtoll(value, nullptr, 10);
    } else if ( !strcmp(name, "scale") ) {
      scale = atoi(value);
      if (scale > 1600) {
        Warning("Limiting scale to 16x");
        scale = 1600;
      }
    } else if ( !strcmp(name, "rate") ) {
      rate = atoi(value);
    } else if ( !strcmp(name, "maxfps") ) {
      maxfps = atof(value);
    } else if ( !strcmp(name, "bitrate") ) {
      bitrate = atoi(value);
    } else if ( !strcmp(name, "ttl") ) {
      ttl = atoi(value);
    } else if ( !strcmp(name, "replay") ) {
      if ( !strcmp(value, "gapless") ) {
        replay = EventStream::MODE_ALL_GAPLESS;
      } else if ( !strcmp(value, "all") ) {
        replay = EventStream::MODE_ALL;
      } else if ( !strcmp(value, "none") ) {
        replay = EventStream::MODE_NONE;
      } else if ( !strcmp(value, "single") ) {
        replay = EventStream::MODE_SINGLE;
      } else {
        Error("Unsupported value %s for replay, defaulting to none", value);
      }
    } else if ( !strcmp(name, "connkey") ) {
      connkey = atoi(value);
    } else if ( !strcmp(name, "buffer") ) {
      playback_buffer = atoi(value);
    } else if ( !strcmp(name, "auth") ) {
      strncpy(auth, value, sizeof(auth)-1);
    } else if ( !strcmp(name, "token") ) {
      jwt_token_str = value;
      Debug(1, "ZMS: JWT token found: %s", jwt_token_str.c_str());
    } else if ( !strcmp(name, "user") ) {
      username = UriDecode(value);
    } else if ( !strcmp(name, "username") ) {
      username = UriDecode(value);
    } else if ( !strcmp(name, "pass") ) {
      password = UriDecode(value);
      Debug(1, "Have %s for password", password.c_str());
    } else if ( !strcmp(name, "password") ) {
      password = UriDecode(value);
      Debug(1, "Have %s for password", password.c_str());
    } else {
      Debug(1, "Unknown parameter passed to zms %s=%s", name, value);
    }  // end if possible parameter names
  }  // end foreach parm

  if ( event_id ) {
    snprintf(log_id_string, sizeof(log_id_string), "zms_e%" PRIu64, event_id);
  } else if ( monitor_id ) {
    snprintf(log_id_string, sizeof(log_id_string), "zms_m%d", monitor_id);
  }
  logInit(log_id_string);

  if ( config.opt_use_auth ) {
    User *user = nullptr;

    if ( jwt_token_str != "" ) {
      // user = zmLoadTokenUser(jwt_token_str, config.auth_hash_ips);
      user = zmLoadTokenUser(jwt_token_str, false);
    } else if ( strcmp(config.auth_relay, "none") == 0 ) {
      if ( checkUser(username.c_str()) ) {
        user = zmLoadUser(username.c_str());
      } else {
        Error("Bad username");
      }
    } else {
      if ( *auth ) {
        user = zmLoadAuthUser(auth, username, config.auth_hash_ips);
      } else if ( username.length() && password.length() ) {
        user = zmLoadUser(username.c_str(), password.c_str());
      }
    }
    if ( !user ) {
      fputs("HTTP/1.0 403 Forbidden\r\n\r\n", stdout);

      const char *referer = getenv("HTTP_REFERER");
      const char *request_uri = getenv("REQUEST_URI");
      const char *xff = getenv("HTTP_X_FORWARDED_FOR");
      const char *remote = getenv("REMOTE_ADDR");
      // Most failures here are stale auth hashes on long-lived <img src=nph-zms?...>
      // streams whose hash TTL expired; the browser keeps reconnecting with the
      // baked-in URL. Including user/auth-prefix/uri/xff makes the noise diagnosable
      // without flipping on Debug.
      char auth_prefix[9] = {0};
      if (*auth) strncpy(auth_prefix, auth, sizeof(auth_prefix)-1);
      Warning("Unable to authenticate user (user='%s' auth='%s%s' uri='%s' referer='%s' xff='%s' remote='%s')",
              username.c_str(),
              auth_prefix,
              (*auth && strlen(auth) > 8) ? "..." : "",
              request_uri ? request_uri : "",
              referer ? referer : "",
              xff ? xff : "",
              remote ? remote : "");
      return exit_zm(0);
    }
    if ( !ValidateAccess(user, monitor_id) ) {
      delete user;
      user = nullptr;
      fputs("HTTP/1.0 403 Forbidden\r\n\r\n", stdout);
      return exit_zm(0);
    }
    delete user;
    user = nullptr;
  }  // end if config.opt_use_auth

  HwCapsDetect();
  Image::Initialise();
  zmSetDefaultTermHandler();
  zmSetDefaultDieHandler();

  setbuf(stdout, nullptr);
  if ( nph ) {
    fputs("HTTP/1.0 200 OK\r\n", stdout);
  }
  fprintf(stdout, "Server: ZoneMinder Video Server/%s\r\n", ZM_VERSION);

  time_t now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
  char date_string[64];
  tm now_tm = {};
  strftime(date_string, sizeof(date_string)-1,
           "%a, %d %b %Y %H:%M:%S GMT", gmtime_r(&now, &now_tm));

  fputs("Last-Modified: ", stdout);
  fputs(date_string, stdout);
  fputs(
    "\r\nExpires: Mon, 26 Jul 1997 05:00:00 GMT\r\n"
    "Cache-Control: no-store, no-cache, must-revalidate\r\n"
    "Cache-Control: post-check=0, pre-check=0\r\n"
    "Pragma: no-cache\r\n",
    stdout);

  if ( source == ZMS_MONITOR ) {
    MonitorStream stream;
    stream.setStreamScale(scale);
    stream.setStreamReplayRate(rate);
    stream.setStreamMaxFPS(maxfps);
    stream.setStreamTTL(ttl);
    stream.setStreamQueue(connkey);
    stream.setStreamBuffer(playback_buffer);
    stream.setFramesToSend(frames_to_send);
    stream.setStreamStart(monitor_id);
    stream.setStreamFrameType(analysis_frames ? StreamBase::FRAME_ANALYSIS: StreamBase::FRAME_NORMAL);

    if ( mode == ZMS_JPEG ) {
      stream.setStreamType(MonitorStream::STREAM_JPEG);
    } else if ( mode == ZMS_RAW ) {
      stream.setStreamType(MonitorStream::STREAM_RAW);
    } else if ( mode == ZMS_ZIP ) {
      stream.setStreamType(MonitorStream::STREAM_ZIP);
    } else if ( mode == ZMS_SINGLE ) {
      stream.setStreamType(MonitorStream::STREAM_SINGLE);
    } else {
      stream.setStreamFormat(format);
      stream.setStreamBitrate(bitrate);
      stream.setStreamType(MonitorStream::STREAM_MPEG);
    }
    stream.runStream();
  } else if ( source == ZMS_FIFO ) {
    FifoStream stream;
    stream.setStreamMaxFPS(maxfps);
    stream.setStreamStart(monitor_id, format);
    stream.runStream();
  } else if ( source == ZMS_EVENT ) {
    EventStream stream;
    stream.setStreamScale(scale);
    stream.setStreamReplayRate(rate);
    stream.setStreamMaxFPS(maxfps);
    stream.setStreamMode(replay);
    stream.setStreamQueue(connkey);
    stream.setFramesToSend(frames_to_send);
    if ( monitor_id && (event_time != std::chrono::system_clock::time_point::min())) {
      stream.setStreamStart(monitor_id, event_time);
    } else if ( event_id && (event_time != std::chrono::system_clock::time_point::min())) {
      stream.setStreamStart(event_id, event_time);
    } else {
      Debug(3, "Setting stream start to frame (%d)", frame_id);
      stream.setStreamStart(event_id, frame_id);
    }
    stream.setStreamFrameType(analysis_frames ? StreamBase::FRAME_ANALYSIS: StreamBase::FRAME_NORMAL);
    if ( mode == ZMS_JPEG ) {
      stream.setStreamType(EventStream::STREAM_JPEG);
    } else if ( mode == ZMS_SINGLE ) {
      stream.setStreamType(MonitorStream::STREAM_SINGLE);
    } else {
      stream.setStreamFormat(format);
      stream.setStreamBitrate(bitrate);
      stream.setStreamType(EventStream::STREAM_MPEG);
    }  // end if jpeg or mpeg
    stream.runStream();
  } else {
    Error("Neither a monitor or event was specified.");
  }  // end if monitor or event

  return exit_zm(0);
}

