#include "HTTPSHandler.h"

HTTPSHandler::HTTPSHandler(const char *serverCert)
{
    _client.setCACert(serverCert);
}

bool HTTPSHandler::connect(const char *host, int port, const char *serverCert)
{
    _host = host;
    _port = port;
    if (serverCert != nullptr)
    {
        _client.setCACert(serverCert);
    }
    _currentState = HttpsState::CONNECTING;
    // reset response
    _response = "";
    return _connect();
}

bool HTTPSHandler::_connect()
{
    return _client.connect(_host, _port);
}

int HTTPSHandler::_getContentLength()
{
    unsigned long startTime = millis();

    String line;
    int contentLength = -1;

    if (!_client.connected())
    {
        Serial.print("Not connected when getting content length");
        _currentState = HttpsState::ERROR;
        return -1;
    }

    while (contentLength == -1)
    {
        while (_client.available())
        {
            line = _client.readStringUntil('\n');
            line.trim(); // Remove any trailing whitespace

            if (line.startsWith("Content-Length: "))
            {
                contentLength = line.substring(15).toInt();
            }

            // Empty line / end of headers
            if (line.length() == 0)
            {
                break;
            }
        }

        // Check for timeout
        if (millis() - startTime > _responseTimeout * 1000)
        {
            Serial.println("Timeout while trying to get Content-Length!");
            _currentState = HttpsState::ERROR;
            return -1;
        }
    }

    return contentLength;
}

void HTTPSHandler::getRequest(const char *path)
{
    // Check if the client is connected before sending
    if (!_client.connected())
    {
        Serial.println("Error: Client is not connected!");
        _currentState = HttpsState::ERROR;
        return;
    }

    String request = "GET " + String(path) + " HTTP/1.1\r\n";
    request += "Host: " + String(_host) + "\r\n";

    if (_customHeader.length() > 0)
    {
        request += _customHeader + "\r\n";
    }

    request += "Connection: keep-alive\r\n\r\n";

    size_t bytesSent = _client.print(request);

    // Check if all bytes of the request were sent
    if (bytesSent != request.length())
    {
        Serial.println("Error: Not all bytes sent!");
        _currentState = HttpsState::ERROR;
        return;
    }

    _currentState = HttpsState::WAITING;
    _requestType = RequestType::GET;
}

void HTTPSHandler::_processDownload(File &dest)
{
    const uint32_t BUFFER_SIZE = 2048;
    int contentLength = _getContentLength();
    static int bytesRead = 0; // Number of bytes downloaded so far
    byte buffer[BUFFER_SIZE];
    unsigned long startMillis = millis();

    if (contentLength <= 0)
    {
        _currentState = HttpsState::ERROR;
    }

    while (_currentState == HttpsState::WAITING)
    {
        // Then proceed to download content
        while (_client.available())
        {
            // If no data is received for the duration of _timeout, it times out
            if (millis() - startMillis > _downloadTimeout * 1000)
            {
                Serial.println("Data inactivity timeout!");
                _currentState = HttpsState::ERROR;
                dest.close();
                close();
                return;
            }
            uint32_t bytesAvailable = _client.available();
            int bytesToRead = min(bytesAvailable, BUFFER_SIZE);

            int bytesReadFromClient = _client.read(buffer, bytesToRead);
            dest.write(buffer, bytesReadFromClient);
            bytesRead += bytesReadFromClient;
        }

        if (_progressCallback)
        {
            _progressCallback(bytesRead, contentLength);
        }

        if (bytesRead >= contentLength)
        {
            dest.close();
            _currentState = HttpsState::COMPLETE;
            close();
            return;
        }
    }
}

String HTTPSHandler::_extractFileName(const String &fullPath)
{
    int lastSlashIndex = fullPath.lastIndexOf('/');

    if (lastSlashIndex == -1)
    {
        // No slashes found, return the full path itself.
        return fullPath;
    }

    // Return the substring from the last slash onwards.
    return fullPath.substring(lastSlashIndex + 1);
}

String HTTPSHandler::_createBoundary(const String &fileNameToUpload)
{
    // Build the multipart form data request
    String boundary = "--" + _BOUNDARY + "\r\n";
    boundary += "Content-Disposition: form-data; name=\"fileToUpload\"; filename=\"" + fileNameToUpload + "\"\r\n";
    boundary += "Content-Type: application/json\r\n";
    boundary += "\r\n";
    return boundary;
}

bool HTTPSHandler::uploadFile(const char *destPath, File &source, const String &fileName)
{
    String fileNameToUpload = fileName == "" ? _extractFileName(source.name()) : fileName;
    Serial.println("Uploading file: " + fileNameToUpload);

    // Needed for header
    String boundary = _createBoundary(fileNameToUpload);
    String endBoundary = "\r\n--" + _BOUNDARY + "--\r\n";

    // Calculate content length
    int contentLength = boundary.length() + endBoundary.length() + source.size();

    // Start sending the request
    _client.println("POST " + String(destPath) + " HTTP/1.1");
    _client.println("Host: appdig.com");
    if (_customHeader.length() > 0)
    {
        _client.println(_customHeader);
    }
    _client.print("Content-Length: " + String(contentLength) + "\r\n");
    _client.println("Content-Type: multipart/form-data; boundary=" + _BOUNDARY);
    _client.println();
    _client.print(boundary);

    const int CHUNK_SIZE = 1024;
    const long startTime = millis();
    // Start sending body in chunks
    while (source.available())
    {
        // Check for timeout
        if (millis() - startTime > _responseTimeout * 1000)
        {
            Serial.println("Timeout while trying to get Content-Length!");
            _currentState = HttpsState::ERROR;
            return 0;
        }
        byte buffer[CHUNK_SIZE];
        int bytesRead = source.read(buffer, CHUNK_SIZE);
        _client.write(buffer, bytesRead);

        if (_progressCallback)
        {
            _progressCallback(bytesRead, contentLength);
        }
    }

    _client.print(endBoundary);

    _currentState = HttpsState::COMPLETE;
    // give the server some time to process the request
    // update in the future to handle response from server
    delay(10);
    close();
    return 1;
}

bool HTTPSHandler::postRequest(const char *destPath, const String &postData)
{
    _client.println("POST " + String(destPath) + " HTTP/1.1");
    _client.println("Host: " + String(_host));
    _client.println("Content-Length: " + String(postData.length()));
    _client.println("Connection: close");
    _client.println();
    _client.println(postData);

    _currentState = HttpsState::WAITING;
    _requestType = RequestType::POST;
    return 1;
}

void HTTPSHandler::_processResponse()
{
    unsigned long startMillis = millis();
    // Read available data from the client and append to _response
    while (_client.available())
    {
        // If no data is received for the duration of _timeout, it times out
        if (millis() - startMillis > _responseTimeout * 1000)
        {
            Serial.println("Data inactivity timeout");
            _currentState = HttpsState::ERROR;
            close();
            return;
        }

        char c = _client.read();
        Serial.print(c);
        _response += c;

        if (_response.endsWith("\r\n0\r\n"))
        {
            break;
        }
    }

    if (!_client.connected() || _response.endsWith("\r\n0\r\n"))
    {
        _currentState = HttpsState::COMPLETE;
    }
    close();
}

void HTTPSHandler::setTarget(const char *sourcePath, File &destFile)
{
    _path = sourcePath;
    _destinationFile = destFile;
    _currentState = HttpsState::WAITING;
    getRequest(_path);
    _requestType = RequestType::DOWNLOAD;
}

void HTTPSHandler::advanceStateMachine()
{
    static int lastState = -1;
    if (lastState != static_cast<int>(_currentState))
    {
        Serial.println("HTTPSHandler state changed to " + static_cast<int>(_currentState));
        lastState = static_cast<int>(_currentState);
    }

    switch (_currentState)
    {
    case HttpsState::CONNECTING:
        if (!_client.connected())
        {
            Serial.println("Could not connect to server");
            _currentState = HttpsState::ERROR;
        }
        _currentState = HttpsState::WAITING;
        break;

    case HttpsState::WAITING:
        switch (_requestType)
        {
        case RequestType::DOWNLOAD:
            _processDownload(_destinationFile);
            break;

        default:
            _processResponse();
            break;
        }
        break;

    case HttpsState::ERROR:
        _currentState = HttpsState::IDLE;
        break;

    case HttpsState::COMPLETE:
        _currentState = HttpsState::IDLE;
        break;

    case HttpsState::IDLE:
        break;

    default:
        break;
    }
}

// void HTTPSHandler::get(const String &url, Callback callback)
// {
//     if (!_client.connect(_host, _port))
//     {
//         Serial.println("Could not connect to server");
//         return;
//     }

//     _client.println("GET / HTTP/1.1");
//     _client.println("HOST appdig.com");
//     _client.println("Connection: close");

//     _requestStarted = 1;
//     _callback = callback;
// }

void HTTPSHandler::close()
{
    if (_client.connected())
    {
        _client.stop();
    }
}

HTTPSHandler::~HTTPSHandler()
{
    close();
}
