/*

    API.ino

    This script handle API requests
    functions.
*/

/* Utilities Functions */
String GetPara(AsyncWebServerRequest * request, String key) {
  if (request->hasParam(key)) {
    return request->getParam(key)->value();
  }
  return "";
}

void SendErrorResp(AsyncWebServerRequest * r, String errorMessage) {
  //Parse the error message into json
  StaticJsonDocument<200> jsonDocument;
  JsonObject root = jsonDocument.to<JsonObject>();
  root["error"] = errorMessage;
  String jsonString;
  serializeJson(root, jsonString);

  //Send it out with request handler
  r->send(200, "application/json", jsonString);
}

void SendJsonResp(AsyncWebServerRequest * r, String jsonString) {
  r->send(200, "application/json", jsonString);
}

void SendOK(AsyncWebServerRequest *r) {
  r->send(200, "application/json", "\"ok\"");
}

//Handle auth check if the request has been authed.
//Return false if user is not authed
bool HandleAuth(AsyncWebServerRequest *request) {
  //Handle for API calls authentication validate
  if (!IsUserAuthed(request)) {
    //user not logged in
    request->send(401, "text/html", "401 - Unauthorized");
    return false;
  }

  return true;
}

/*

    Handler Functions

    These are API endpoints handler
    which handle special API call
    for backend operations

*/

/* Authentications */
//Check if the user has logged in
void HandleCheckAuth(AsyncWebServerRequest *r) {
  if (IsUserAuthed(r)) {
    SendJsonResp(r, "true");
  } else {
    SendJsonResp(r, "false");
  }
}

//Handle login request
void HandleLogin(AsyncWebServerRequest *r) {
  String username = GetPara(r, "username");
  String password = GetPara(r, "password");
  if (adminUsername == "") {
    SendErrorResp(r, "admin account not enabled");
    return;
  }
  if (username.equals(adminUsername) && password.equals(adminPassword)) {
    //Username and password correct. Set a cookie for this login.
    //Generate a unique cookie for this login session
    String cookieId = GeneratedRandomHex();
    Serial.print("Generating new cookie ID ");
    Serial.println(cookieId);

    String expireUTC = getUTCTimeString(getTime() + 604800);
    Serial.print("Generating expire UTC timestamp ");
    Serial.println(expireUTC);

    AsyncWebServerResponse *response = r->beginResponse(200, "application/json", "\"ok\"");
    response->addHeader("Server", mdnsName);
    response->addHeader("Cache-Control", "no-cache");
    response->addHeader("Set-Cookie",  "web-auth=" + cookieId + "; Path=/; Expires=" + expireUTC + "; Max-Age=604800");

    //Save the cookie id
    DBWrite("auth", "cookie", cookieId);
    authSession = cookieId;

    //Return login succ
    r->send(response);

    Serial.println(username + " logged in");
  } else {
    SendErrorResp(r, "invalid username or password");
    return;
  }
  SendOK(r);
}

//Handle logout request, or you can logout with
//just front-end by going to log:out@{ip_addr}/api/auth/logout
void HandleLogout(AsyncWebServerRequest *r) {
  if (!IsUserAuthed(r)) {
    SendErrorResp(r, "not logged in");
    return;
  }

  //Remove the cookie on client side
  AsyncWebServerResponse *response = r->beginResponse(200, "application/json", "\"ok\"");
  response->addHeader("Server", mdnsName);
  response->addHeader("Cache-Control", "no-cache");
  response->addHeader("Set-Cookie",  "web-auth=deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT");
  r->send(response);

  //Delete the server side cookie
  DBRemove("auth", "cookie");
  authSession = "";
}

/* File System Functions */
//HandleListDir handle the listing of directory under /www/
void HandleListDir(AsyncWebServerRequest *r) {
  if (!HandleAuth(r)) {
    return;
  }

  //Get the folder path to be listed
  //As ESP8266 dont have enough memory for proper struct to json conv, we are hacking a json string out of a single for-loop
  String jsonString = "[";
  String folderPath = GetPara(r, "dir");
  folderPath = "/www" + folderPath;
  if (SD.exists(folderPath)) {
    File root = SD.open(folderPath);
    bool firstObject = true;
    if (root) {
      while (true) {
        File entry = root.openNextFile();
        if (!entry) {
          // No more files
          break;
        } else {
          //There are more lines. Add a , to the end of the previous json object
          if (!firstObject) {
            jsonString = jsonString + ",";
          } else {
            firstObject = false;
          }

        }

        String isDirString = "true";
        if (!entry.isDirectory()) {
          isDirString = "false";
        }
        jsonString = jsonString + "{\"Filename\":\"" + entry.name() + "\",\"Filesize\":" + String(entry.size()) + ",\"IsDir\":" + isDirString + "}";
        entry.close();
      }
      root.close();

      jsonString += "]";
      SendJsonResp(r, jsonString);
    } else {
      SendErrorResp(r, "path read failed");
    }
  } else {
    SendErrorResp(r, "path not found");
  }
  Serial.println(folderPath);
}

//Handle file rename on SD card
void HandleFileRename(AsyncWebServerRequest *r) {
  if (!HandleAuth(r)) {
    return;
  }
  String srcFile = GetPara(r, "src");
  String destFile = GetPara(r, "dest");
  srcFile.trim();
  destFile.trim();
  srcFile = "/www" + srcFile;
  destFile = "/www" + destFile;
  if (!SD.exists(srcFile)) {
    SendErrorResp(r, "source file not exists");
    return;
  }

  if (SD.exists(destFile)) {
    SendErrorResp(r, "destination file already exists");
    return;
  }

  SD.rename(srcFile, destFile);
  SendOK(r);
}


//Handle file delete on SD card
void HandleFileDel(AsyncWebServerRequest *r) {
  if (!HandleAuth(r)) {
    return;
  }
  String targetFile = GetPara(r, "target");
  targetFile.trim();
  if (targetFile.equals("/")) {
    //Do not allow remove the whole root folder
    SendErrorResp(r, "you cannot remove the root folder");
    return;
  }
  targetFile = "/www" + targetFile;
  if (!SD.exists(targetFile)) {
    SendErrorResp(r, "target file not exists");
    return;
  }



  if (!SD.remove(targetFile)) {
    if (IsDir(targetFile)) {
      if (!targetFile.endsWith("/")) {
        //pad the tailing slash
        targetFile = targetFile + "/";
      }

      //Try remove dir
      if (!SD.rmdir(targetFile)) {
        //The folder might contain stuffs. Do recursive delete
        Serial.println("rmdir failed. Trying recursive delete");
        if (recursiveDirRemove(targetFile)) {
          SendOK(r);
        } else {
          SendErrorResp(r, "folder delete failed");
        }

      } else {
        SendOK(r);
      }
    } else {
      SendErrorResp(r, "file delete failed");
    }
    return;
  }

  SendOK(r);
}

//Hanle creating new Folder
void HandleNewFolder(AsyncWebServerRequest *r) {
  if (!HandleAuth(r)) {
    return;
  }

  //Get and clean the folder path
  String folderPath = GetPara(r, "path");
  folderPath.trim();
  if (folderPath == "") {
    SendErrorResp(r, "invalid folder path given");
    return;
  }

  folderPath = "/www" + folderPath;

  if (SD.exists(folderPath)) {
    //Already exists
    SendErrorResp(r, "folder with given path already exists");
    return;
  }

  SD.mkdir(folderPath);
  SendOK(r);
}

//Handle download file from any path, including the private store folder
void HandleFileDownload(AsyncWebServerRequest *r) {
  if (!HandleAuth(r)) {
    return;
  }
  String targetFile = GetPara(r, "file");
  String preview = GetPara(r, "preview");
  if (!SD.exists("/www" + targetFile)) {
    r->send(404, "text/html", "404 - File Not Found");
    return;
  }

  //Check if it is dir, ESP8266 have no power of creating zip file
  if (IsDir("/www" + targetFile)) {
    r->send(500, "text/html", "500 - Internal Server Error: Target is a folder");
    return;
  }

  //Ok. Proceed with file serving
  if (preview == "true") {
    //Serve
    r->send(SDFS, "/www" + targetFile, getMime(targetFile), false);
  } else {
    //Download
    r->send(SDFS, "/www" + targetFile, "application/octet-stream", false);
  }

}

//Get the file / folder properties
void HandleFileProp(AsyncWebServerRequest *r) {
  if (!HandleAuth(r)) {
    return;
  }
  String filepath = GetPara(r, "file");
  filepath.trim();
  String realFilepath = "/www" + filepath;

  String resp = "";
  if (IsDir(realFilepath)) {
    //target is a folder
    uint32_t totalSize = 0;
    uint16_t fileCount = 0;
    uint16_t folderCount = 0;
    analyzeDirectory(realFilepath, totalSize, fileCount, folderCount);
    resp = "{\"filename\":\"" + basename(filepath) + "\",\"filepath\":\"" + filepath + "\",\"isDir\":true,\"filesize\":" + String(totalSize) + ",\"fileCounts\":" + String(fileCount) + ",\"folderCounts\":" + String(folderCount) + "}";

  } else {
    //target is a file
    File targetFile = SD.open(realFilepath);
    if (!targetFile) {
      SendErrorResp(r, "File open failed");
      return;
    }

    resp = "{\"filename\":\"" + basename(filepath) + "\",\"filepath\":\"" + filepath + "\",\"isDir\":false,\"filesize\":" + String(targetFile.size()) + "}";
    targetFile.close();
  }

  SendJsonResp(r, resp);
}

//Search with basic keyword match
void HandleFileSearch(AsyncWebServerRequest *r) {
  if (!HandleAuth(r)) {
    return;
  }
  String keyword = GetPara(r, "keyword");
  keyword.trim();
  if (keyword.equals("")) {
    SendErrorResp(r, "keyword cannot be empty");
    return;
  }

  //Prepare for the resp
  AsyncResponseStream *response = r->beginResponseStream("application/json");
  response->print("[");

  //Recursive search for the whole /www/ directory (Require tailing slash)
  int foundCounter = 0;
  scanSDCardForKeyword("/www/", keyword, &foundCounter, response);

  //Send the resp
  response->print("]");
  r->send(response);
}
/* Handle Load and Set of Prefernece Value */
//Set Preference, auth user only
void HandleSetPref(AsyncWebServerRequest *r) {
  if (!HandleAuth(r)) {
    return;
  }
  String key = GetPara(r, "key");
  String val = GetPara(r, "value");
  key.trim();
  val.trim();

  if (key == "" || val == "") {
    SendErrorResp(r, "invalid key or value given");
    return;
  }

  DBWrite("pref", key, val);
  SendOK(r);
}

//Load Prefernece, allow public access
void HandleLoadPref(AsyncWebServerRequest *r) {
  String key = GetPara(r, "key");
  key.trim();
  if (!DBKeyExists("pref", key)) {
    SendErrorResp(r, "preference with given key not found");
    return;
  }

  String prefValue = DBRead("pref", key);
  r->send(200, "application/json", "\"" + prefValue + "\"");
}

/* Handle System Info */
void HandleLoadSpaceInfo(AsyncWebServerRequest *r) {
  String jsonResp = "{\
    \"diskSpace\":" + String(getSDCardTotalSpace()) + ",\
    \"usedSpace\": " + String(getSDCardUsedSpace()) + "\
  }";

  SendJsonResp(r, jsonResp);
}

//Get the current connected WiFi info
void HandleWiFiInfo(AsyncWebServerRequest *r) {
  StaticJsonDocument<256> jsonBuffer;
  jsonBuffer["SSID"] = WiFi.SSID();
  jsonBuffer["WifiStatus"] = WiFi.status();
  jsonBuffer["WifiStrength"] = WiFi.RSSI();
  jsonBuffer["MAC"] = WiFi.macAddress();
  jsonBuffer["IP"] = WiFi.localIP().toString();
  jsonBuffer["Subnet"] = WiFi.subnetMask().toString();
  jsonBuffer["Gateway"] = WiFi.gatewayIP().toString();
  jsonBuffer["DNS1"] = WiFi.dnsIP(0).toString();
  jsonBuffer["DNS2"] = WiFi.dnsIP(1).toString();
  jsonBuffer["DNS3"] = WiFi.dnsIP(2).toString();

  // Serialize the JSON buffer to a string
  String jsonString;
  serializeJson(jsonBuffer, jsonString);
  SendJsonResp(r, jsonString);
}