#ifndef LORGHIC_LICENSE_MQ4
#define LORGHIC_LICENSE_MQ4

// Lorghic Marketplace terminal license client for MetaTrader 4.
// No marketplace server secret is required or permitted in this file.

class CLorghicLicense
{
private:
   string   m_base_url;
   string   m_product;
   string   m_version;
   string   m_license_key;
   string   m_hwid;
   string   m_token;
   datetime m_valid_until;
   string   m_last_code;
   string   m_last_message;
   int      m_timeout_ms;

   string JsonEscape(string value)
   {
      StringReplace(value, "\\", "\\\\");
      StringReplace(value, "\"", "\\\"");
      StringReplace(value, "\r", "\\r");
      StringReplace(value, "\n", "\\n");
      StringReplace(value, "\t", "\\t");
      return value;
   }

   bool JsonString(const string json,const string key,string &value)
   {
      string marker="\""+key+"\":";
      int start=StringFind(json,marker);
      if(start<0) return false;
      start+=StringLen(marker);
      while(start<StringLen(json) && StringGetCharacter(json,start)<=32) start++;
      if(start>=StringLen(json) || StringGetCharacter(json,start)!=34) return false;
      start++;
      string output="";
      bool escaped=false;
      for(int i=start;i<StringLen(json);i++)
      {
         ushort character=StringGetCharacter(json,i);
         if(escaped)
         {
            if(character==110) output+="\n";
            else if(character==114) output+="\r";
            else if(character==116) output+="\t";
            else output+=ShortToString(character);
            escaped=false;
         }
         else if(character==92) escaped=true;
         else if(character==34)
         {
            value=output;
            return true;
         }
         else output+=ShortToString(character);
      }
      return false;
   }

   bool JsonBool(const string json,const string key,bool &value)
   {
      string marker="\""+key+"\":";
      int start=StringFind(json,marker);
      if(start<0) return false;
      start+=StringLen(marker);
      while(start<StringLen(json) && StringGetCharacter(json,start)<=32) start++;
      if(StringSubstr(json,start,4)=="true")
      {
         value=true;
         return true;
      }
      if(StringSubstr(json,start,5)=="false")
      {
         value=false;
         return true;
      }
      return false;
   }

   bool JsonLong(const string json,const string key,long &value)
   {
      string marker="\""+key+"\":";
      int start=StringFind(json,marker);
      if(start<0) return false;
      start+=StringLen(marker);
      while(start<StringLen(json) && StringGetCharacter(json,start)<=32) start++;
      int finish=start;
      while(finish<StringLen(json))
      {
         ushort character=StringGetCharacter(json,finish);
         if(character<48 || character>57) break;
         finish++;
      }
      if(finish<=start) return false;
      value=StringToInteger(StringSubstr(json,start,finish-start));
      return true;
   }

   void ReadApiError(const string response,const int http_code)
   {
      string value;
      m_last_code="http_"+IntegerToString(http_code);
      m_last_message="License request failed with HTTP "+IntegerToString(http_code)+".";
      if(JsonString(response,"code",value)) m_last_code=value;
      if(JsonString(response,"message",value)) m_last_message=value;
   }

   bool PostJson(const string endpoint,const string body,string &response,int &http_code)
   {
      char payload[];
      char result[];
      string response_headers="";
      string headers="Content-Type: application/json\r\nAccept: application/json\r\n";
      StringToCharArray(body,payload,0,WHOLE_ARRAY,CP_UTF8);
      int payload_size=ArraySize(payload);
      if(payload_size>0 && payload[payload_size-1]==0) ArrayResize(payload,payload_size-1);
      ResetLastError();
      http_code=WebRequest("POST",m_base_url+endpoint,headers,m_timeout_ms,payload,result,response_headers);
      if(http_code<0)
      {
         int error=GetLastError();
         m_last_code="network_error";
         if((bool)MQLInfoInteger(MQL_TESTER))
            m_last_message="MetaTrader does not allow WebRequest in the Strategy Tester. Activate on a live or demo terminal chart.";
         else if(error==4014)
            m_last_message="Enable WebRequest for "+m_base_url+" in Tools > Options > Expert Advisors.";
         else
            m_last_message="License server connection failed. MetaTrader error "+IntegerToString(error)+".";
         return false;
      }
      response=CharArrayToString(result,0,WHOLE_ARRAY,CP_UTF8);
      return true;
   }

   string LoadOrCreateDeviceId()
   {
      string filename="LorghicMarketplace\\terminal-device.id";
      int handle=FileOpen(filename,FILE_READ|FILE_TXT|FILE_ANSI|FILE_COMMON);
      if(handle!=INVALID_HANDLE)
      {
         string existing=FileReadString(handle);
         FileClose(handle);
         if(StringLen(existing)>=8) return existing;
      }
      MathSrand((int)(GetTickCount()^TimeLocal()));
      string generated="terminal-"+IntegerToString((int)TimeLocal())+"-"+
                       IntegerToString(MathRand())+"-"+IntegerToString(MathRand())+"-"+
                       IntegerToString(MathRand());
      handle=FileOpen(filename,FILE_WRITE|FILE_TXT|FILE_ANSI|FILE_COMMON);
      if(handle!=INVALID_HANDLE)
      {
         FileWriteString(handle,generated);
         FileClose(handle);
      }
      return generated;
   }

   string RuntimeContext(const string credential_name,const string credential_value)
   {
      return "\""+credential_name+"\":\""+JsonEscape(credential_value)+"\","+
             "\"product\":\""+JsonEscape(m_product)+"\","+
             "\"platform\":\"mt4\","+
             "\"productVersion\":\""+JsonEscape(m_version)+"\","+
             "\"terminalBuild\":"+IntegerToString((int)TerminalInfoInteger(TERMINAL_BUILD))+","+
             "\"mtAccountNumber\":\""+IntegerToString(AccountNumber())+"\","+
             "\"brokerServer\":\""+JsonEscape(AccountServer())+"\","+
             "\"hwid\":\""+JsonEscape(m_hwid)+"\"";
   }

   bool AcceptResponse(const string response,const int http_code)
   {
      if(http_code!=200)
      {
         ReadApiError(response,http_code);
         if(http_code==400 || http_code==401 || http_code==403 || http_code==409 || http_code==426)
         {
            m_token="";
            m_valid_until=0;
         }
         return false;
      }
      bool active=false;
      string token="";
      long valid_until_unix=0;
      if(!JsonBool(response,"active",active) || !active ||
         !JsonString(response,"validationToken",token) ||
         !JsonLong(response,"validUntilUnix",valid_until_unix))
      {
         m_last_code="invalid_response";
         m_last_message="License server returned an invalid response.";
         return false;
      }
      m_token=token;
      m_valid_until=(datetime)valid_until_unix;
      JsonString(response,"code",m_last_code);
      m_last_message="License valid until "+TimeToString(m_valid_until,TIME_DATE|TIME_SECONDS)+" GMT.";
      return true;
   }

public:
   CLorghicLicense()
   {
      m_base_url="https://market.lorghic.com";
      m_timeout_ms=10000;
      m_valid_until=0;
      m_last_code="not_initialized";
      m_last_message="License client is not initialized.";
   }

   void Configure(const string product,const string version,const string license_key,const string hwid="")
   {
      m_product=product;
      m_version=version;
      m_license_key=license_key;
      m_hwid=StringLen(hwid)>=8 ? hwid : LoadOrCreateDeviceId();
      m_token="";
      m_valid_until=0;
      m_last_code="ready";
      m_last_message="Ready to activate.";
   }

   void SetBaseUrl(string base_url)
   {
      while(StringLen(base_url)>0 && StringSubstr(base_url,StringLen(base_url)-1,1)=="/")
         base_url=StringSubstr(base_url,0,StringLen(base_url)-1);
      m_base_url=base_url;
   }

   void SetTimeout(const int timeout_ms)
   {
      if(timeout_ms>=1000 && timeout_ms<=60000) m_timeout_ms=timeout_ms;
   }

   bool Activate()
   {
      if(StringLen(m_license_key)<20 || StringLen(m_product)<3 || StringLen(m_version)<5)
      {
         m_last_code="configuration_error";
         m_last_message="Product, version or license key is missing.";
         return false;
      }
      string response="";
      int http_code=0;
      string body="{"+RuntimeContext("licenseKey",m_license_key)+"}";
      if(!PostJson("/api/licenses/activate",body,response,http_code)) return false;
      return AcceptResponse(response,http_code);
   }

   bool Validate()
   {
      if(StringLen(m_token)<80) return Activate();
      string response="";
      int http_code=0;
      string body="{"+RuntimeContext("validationToken",m_token)+"}";
      if(!PostJson("/api/licenses/validate",body,response,http_code))
         return (m_valid_until>TimeGMT());
      bool accepted=AcceptResponse(response,http_code);
      if(!accepted && http_code==401 && m_last_code=="invalid_token")
         return Activate();
      if(!accepted && (http_code==429 || http_code>=500) && m_valid_until>TimeGMT())
      {
         m_last_message+=" Existing validation remains valid until "+TimeToString(m_valid_until,TIME_DATE|TIME_SECONDS)+" GMT.";
         return true;
      }
      return accepted;
   }

   bool EnsureValid(const bool force_refresh=false)
   {
      if(StringLen(m_token)<80) return Activate();
      if(!force_refresh && m_valid_until>TimeGMT()+300) return true;
      return Validate();
   }

   bool IsActive()
   {
      return (StringLen(m_token)>=80 && m_valid_until>TimeGMT());
   }

   string LastCode()       { return m_last_code; }
   string LastMessage()    { return m_last_message; }
   datetime ValidUntil()   { return m_valid_until; }
};

#endif
