#include "ThermoproBLE.hpp"
#include <Arduino.h>

namespace cyd {

ThermoproBLE* ThermoproBLE::instance_ = nullptr;

const NimBLEUUID ThermoproBLE::kServiceUuid = NimBLEUUID("1086FFF0-3343-4817-8BB2-B32206336CE8");
const NimBLEUUID ThermoproBLE::kNotifyUuid  = NimBLEUUID("1086FFF2-3343-4817-8BB2-B32206336CE8");
const NimBLEUUID ThermoproBLE::kWriteUuid   = NimBLEUUID("1086FFF1-3343-4817-8BB2-B32206336CE8");

constexpr uint8_t ThermoproBLE::kHandshake[];

ThermoproBLE::ThermoproBLE() {
    instance_ = this;
}

void ThermoproBLE::setAddress(const String& mac) {
    address_ = mac;
}

const char* ThermoproBLE::getStateName() const {
    switch (state_) {
        case State::Idle: return "idle";
        case State::Scanning: return "scanning";
        case State::Connecting: return "connecting";
        case State::Discovering: return "discovering";
        case State::EnablingNotify: return "enabling-notify";
        case State::SendingHandshake: return "sending-handshake";
        case State::Running: return "running";
        case State::Reconnecting: return "reconnecting";
    }
    return "unknown";
}

bool ThermoproBLE::init(const String& deviceName) {
    NimBLEDevice::init(std::string(deviceName.c_str()));
    // We do not need a security/bonded pairing for the ThermoPro protocol.
    NimBLEDevice::setSecurityAuth(false, false, false);

    if (address_.length() > 0) {
        Serial.printf("ThermoproBLE: BLE initialized as '%s', target MAC %s\n", deviceName.c_str(), address_.c_str());
    } else {
        Serial.printf("ThermoproBLE: BLE initialized as '%s', will scan for ThermoPro service UUID\n", deviceName.c_str());
    }
    state_ = State::Idle;
    return true;
}

bool ThermoproBLE::startConnect() {
    if (state_ != State::Idle && state_ != State::Reconnecting) {
        Serial.println("ThermoproBLE: connect already in progress");
        return false;
    }

    if (NimBLEDevice::getScan()->isScanning()) {
        NimBLEDevice::getScan()->stop();
    }

    Serial.println("ThermoproBLE: starting scan/connect");
    state_ = State::Scanning;
    stateStartMillis_ = millis();
    return scanAndConnect_();
}

void ThermoproBLE::update() {
    switch (state_) {
        case State::Idle:
            // Waiting for an explicit startConnect() call.
            break;

        case State::Scanning:
            // If we found a candidate during the scan, stop scanning and connect.
            if (pendingConnectFound_) {
                if (millis() - pendingConnectSeenMillis_ >= 200) {
                    NimBLEDevice::getScan()->stop();
                    state_ = State::Connecting;
                    stateStartMillis_ = millis();
                    connectToAddress_(pendingConnectAddress_);
                }
                break;
            }

            if (millis() - stateStartMillis_ > 25000) {
                Serial.println("ThermoproBLE: scan timeout, forcing reconnect");
                NimBLEDevice::getScan()->stop();
                setConnected_(false);
                startReconnectDelay_();
            }
            break;

        case State::Connecting:
            if (client_ && client_->isConnected()) {
                Serial.println("ThermoproBLE: connected to server");
                if (!setupConnection_(client_)) {
                    Serial.println("ThermoproBLE: setup failed");
                    cleanupClient_();
                    setConnected_(false);
                    startReconnectDelay_();
                    break;
                }
                state_ = State::Running;
                stateStartMillis_ = millis();
                lastActivityMillis_ = millis();
                reconnectFailures_ = 0;
                reconnectDelayMs_ = 5000;
                break;
            }
            if (millis() - stateStartMillis_ > 25000) {
                Serial.println("ThermoproBLE: connect timeout");
                cleanupClient_();
                setConnected_(false);
                startReconnectDelay_();
            }
            break;

        case State::Discovering:
        case State::EnablingNotify:
        case State::SendingHandshake:
            if (millis() - stateStartMillis_ > 15000) {
                Serial.printf("ThermoproBLE: setup state timeout in state %d\n", static_cast<int>(state_));
                cleanupClient_();
                setConnected_(false);
                startReconnectDelay_();
            }
            break;

        case State::Running: {
            if (!client_ || !client_->isConnected()) {
                Serial.println("ThermoproBLE: connection lost, reconnecting");
                cleanupClient_();
                setConnected_(false);
                startReconnectDelay_();
                break;
            }
            if (millis() - lastActivityMillis_ > 30000) {
                Serial.println("ThermoproBLE: no notifications for 30s, reconnecting");
                cleanupClient_();
                setConnected_(false);
                startReconnectDelay_();
            }
            break;
        }

        case State::Reconnecting:
            if (millis() - stateStartMillis_ >= reconnectDelayMs_) {
                state_ = State::Idle;
                startConnect();
            }
            break;
    }
}

void ThermoproBLE::disconnect() {
    cleanupClient_();
    NimBLEDevice::deinit(true);
    setConnected_(false);
    state_ = State::Idle;
}

void ThermoproBLE::ScanCallbacks::onResult(NimBLEAdvertisedDevice* device) {
    if (ThermoproBLE::instance_ == nullptr) {
        return;
    }
    auto* self = ThermoproBLE::instance_;
    String addr(device->getAddress().toString().c_str());
    String name(device->getName().c_str());

    bool matchesAddress = !self->address_.isEmpty()
                          && addr.equalsIgnoreCase(self->address_);
    bool matchesName = isThermoproDevice_(name);
    bool matchesService = device->isAdvertisingService(ThermoproBLE::kServiceUuid);

    // Verbose scan logging during bringup. Print every device address + name.
    Serial.printf("[ble-scan] %s  rssi=%d  name='%s' service_match=%s\n",
        addr.c_str(), device->getRSSI(), name.c_str(),
        matchesService ? "yes" : "no");

    if (matchesAddress || matchesName || matchesService) {
        Serial.printf("ThermoproBLE: discovered candidate %s (%s), addrType=%d\n",
            name.c_str(), addr.c_str(), device->getAddress().getType());
        // Record the address for a connect initiated from update() once the
        // scan has been stopped cleanly. Don't connect from inside the callback.
        self->pendingConnectAddress_ = device->getAddress();
        self->pendingConnectFound_ = true;
        self->pendingConnectSeenMillis_ = millis();
    }
}

bool ThermoproBLE::scanAndConnect_() {
    pendingConnectFound_ = false;

    NimBLEScan* scanner = NimBLEDevice::getScan();
    if (!callbacksRegistered_) {
        scanner->setAdvertisedDeviceCallbacks(&scanCallbacks_, false);
        callbacksRegistered_ = true;
    }
    scanner->setActiveScan(true);
    scanner->setInterval(100);
    scanner->setWindow(99);
    scanner->start(5, false);
    return true;
}

bool ThermoproBLE::connectToAddress_(const NimBLEAddress& address) {
    state_ = State::Connecting;
    stateStartMillis_ = millis();

    bool addrTypeRandom = (address.getType() == BLE_ADDR_RANDOM);
    Serial.printf("ThermoproBLE: connecting to %s, addrType=%s\n",
        address.toString().c_str(), addrTypeRandom ? "random" : "public");

    // The scan must already be stopped before this is called.
    delay(300);

    client_ = NimBLEDevice::createClient();
    // Conservative connection parameters.
    client_->setConnectionParams(12, 12, 0, 160);
    client_->setConnectTimeout(20);

    bool ok = client_->connect(address, false);
    if (!ok) {
        Serial.println("ThermoproBLE: connect call rejected immediately");
        cleanupClient_();
        setConnected_(false);
        startReconnectDelay_();
        return false;
    }

    Serial.println("ThermoproBLE: connect call accepted");
    return true;
}

void ThermoproBLE::cleanupClient_() {
    if (client_) {
        if (client_->isConnected()) {
            client_->disconnect();
            delay(100);
        }
        NimBLEDevice::deleteClient(client_);
        client_ = nullptr;
    }
    notifyChar_ = nullptr;
    writeChar_ = nullptr;
}

bool ThermoproBLE::setupConnection_(NimBLEClient* client) {
    state_ = State::Discovering;
    stateStartMillis_ = millis();

    NimBLERemoteService* service = client->getService(kServiceUuid);
    if (!service) {
        Serial.println("ThermoproBLE: service not found");
        return false;
    }

    notifyChar_ = service->getCharacteristic(kNotifyUuid);
    writeChar_ = service->getCharacteristic(kWriteUuid);
    if (!notifyChar_ || !writeChar_) {
        Serial.println("ThermoproBLE: required characteristics missing");
        return false;
    }

    state_ = State::EnablingNotify;
    stateStartMillis_ = millis();

    if (!notifyChar_->subscribe(true, notifyCallback_)) {
        Serial.println("ThermoproBLE: subscribe failed");
        return false;
    }

    Serial.println("ThermoproBLE: notifications enabled");

    state_ = State::SendingHandshake;
    stateStartMillis_ = millis();

    if (!writeChar_->writeValue(const_cast<uint8_t*>(kHandshake), sizeof(kHandshake), true)) {
        Serial.println("ThermoproBLE: handshake write failed");
        return false;
    }

    Serial.println("ThermoproBLE: handshake sent");
    return true;
}

void ThermoproBLE::setConnected_(bool connected) {
    bool changed = reading_.connected != connected;
    reading_.connected = connected;
    if (!connected) {
        reading_.battery = 0;
        for (auto& t : reading_.rawTemperatures) {
            t = -999.0f;
        }
    }
    if (changed && stateCallback_) {
        stateCallback_(reading_);
    }
}

void ThermoproBLE::startReconnectDelay_() {
    state_ = State::Reconnecting;
    stateStartMillis_ = millis();
    reconnectFailures_ = min(static_cast<int>(reconnectFailures_) + 1, 6);
    reconnectDelayMs_ = min(5000UL * (1UL << reconnectFailures_), 300000UL);
    Serial.printf("ThermoproBLE: reconnect in %lu ms (failure %d)\n", reconnectDelayMs_, reconnectFailures_);
}

void ThermoproBLE::notifyCallback_(BLERemoteCharacteristic* characteristic, uint8_t* data, size_t length, bool isNotify) {
    (void)characteristic;
    (void)isNotify;
    if (instance_) {
        instance_->handlePacket_(data, length);
    }
}

void ThermoproBLE::handlePacket_(const uint8_t* data, size_t length) {
    if (length < 2) {
        return;
    }

    lastActivityMillis_ = millis();
    uint8_t cmd = data[0];

    switch (cmd) {
        case 0x30:
            parseTemperaturePacket_(data, length);
            break;
        case 0x01:
        case 0x41:
        case 0xE0:
            // Handshake ack / version / keepalive — not temperature data.
            break;
        default:
            break;
    }
}

void ThermoproBLE::parseTemperaturePacket_(const uint8_t* data, size_t length) {
    if (length < 6) {
        return;
    }

    reading_.battery = data[2];
    // data[3] appears to be a unit/flags byte in some captures; we keep the
    // device unit reported by the app/protocol as Celsius by default.
    reading_.deviceUnit = 'C';

    if (data[4] == 0x00) {
        reading_.probeCount = 4;
    } else {
        reading_.probeCount = min(static_cast<uint8_t>(data[4]), static_cast<uint8_t>(4));
    }

    size_t probeOffset = 5;
    for (size_t i = 0; i < 4; ++i) {
        float temp = -999.0f;
        size_t offset = probeOffset + i * 2;
        if (offset + 1 < length) {
            temp = decodeTemperature_(data[offset], data[offset + 1]);
        }
        reading_.rawTemperatures[i] = temp;
    }

    reading_.lastUpdateMillis = millis();
    bool wasConnected = reading_.connected;
    reading_.connected = true;

    Serial.printf("ThermoproBLE: battery=%d%% temps=", reading_.battery);
    for (size_t i = 0; i < 4; ++i) {
        if (isValidTemperature(reading_.rawTemperatures[i])) {
            Serial.printf("%.1f ", reading_.rawTemperatures[i]);
        } else {
            Serial.print("--- ");
        }
    }
    Serial.println();

    if (stateCallback_) {
        stateCallback_(reading_);
    }
}

float ThermoproBLE::decodeTemperature_(uint8_t byte1, uint8_t byte2) {
    if (byte1 == 0xFF && byte2 == 0xFF) {
        return -999.0f;
    }
    if (byte1 == 0xDD && byte2 == 0xDD) {
        return -100.0f;
    }
    if (byte1 == 0xEE && byte2 == 0xEE) {
        return 666.0f;
    }

    bool isNegative = (byte1 & 0x80) != 0;
    int hundreds = ((byte1 & 0x70) >> 4) * 100;
    int tens = (byte1 & 0x0F) * 10;
    int ones = (byte2 & 0xF0) >> 4;
    float decimal = (byte2 & 0x0F) * 0.1f;

    float temp = static_cast<float>(hundreds + tens + ones) + decimal;
    if (isNegative) {
        temp = -temp;
    }
    return temp;
}

uint8_t ThermoproBLE::calculateChecksum_(const uint8_t* data, size_t len) {
    uint16_t sum = 0;
    for (size_t i = 0; i < len; ++i) {
        sum += data[i];
    }
    return static_cast<uint8_t>(sum & 0xFF);
}

bool ThermoproBLE::isThermoproDevice_(const String& name) {
    String lower = name;
    lower.toLowerCase();
    return lower.indexOf("thermo") >= 0 || (name.length() >= 2 && name.startsWith("TP"));
}

} // namespace cyd
