

// === Início de: ./src/server.js ===
// src/server.js
const http = require('http');
const { Server } = require('socket.io');
const jwt = require('jsonwebtoken');

const app = require('./app');
const db = require('./db/knex');
const config = require('./config');

// ✅ mqtt exports { on, ... }
const mqttClient = require('./mqtt');

const { normalizeChargingStatus } = require('./domain/normalize/chargingStatus');
const { normalizeChargingConfig } = require('./domain/normalize/chargingConfig');

const server = http.createServer(app);

const io = new Server(server, {
  cors: {
    origin: config.corsOrigins,
    methods: ['GET', 'POST'],
    credentials: true,
  },
});

console.log('MQTT client exports=', Object.keys(mqttClient || {}));
if (typeof mqttClient?.on !== 'function') {
  console.error('[server] mqttClient.on não existe. Verifica src/mqtt/index.js');
}

// ---------------------------
// auth middleware do socket
// ---------------------------
io.use((socket, next) => {
  const token = socket.handshake.auth?.token;
  if (!token) return next(new Error('Authentication error: token required'));

  try {
    const payload = jwt.verify(token, config.jwtSecret);
    socket.user = payload;
    next();
  } catch (err) {
    next(new Error('Authentication error'));
  }
});

io.on('connection', (socket) => {
  console.log(`Client connected: ${socket.id}, user: ${socket.user.username}`);

  socket.on('joinChargers', async (chargerIds = []) => {
    try {
      if (!Array.isArray(chargerIds) || chargerIds.length === 0) return;

      const rows = await db('chargers')
        .whereIn('id', chargerIds)
        .andWhere({ user_id: socket.user.id })
        .select('id');

      const allowed = rows.map((r) => r.id);
      allowed.forEach((id) => socket.join(id));

      console.log(`Socket ${socket.id} joined chargers: ${allowed}`);
    } catch (err) {
      console.error('joinChargers error:', err);
    }
  });

  socket.on('disconnect', (reason) => {
    console.log(`Client disconnected: ${socket.id}, reason: ${reason}`);
  });
});

// ---------------------------
// Relay MQTT -> Socket.IO
// ---------------------------
if (typeof mqttClient?.on === 'function') {
  mqttClient.on('charging-status', (data) => {
    const normalized = normalizeChargingStatus(data);
    const chargerId = normalized.charger_id;
    if (!chargerId) return;

    io.to(chargerId).emit('charging-status', normalized);
  });

  mqttClient.on('charging-config', (data) => {
    const normalized = normalizeChargingConfig(data);
    const chargerId = normalized.charger_id;
    if (!chargerId) return;

    io.to(chargerId).emit('charging-config', normalized);
  });

  mqttClient.on('scheduler-state', (evt) => {
    const chargerId = evt?.charger_id;
    if (!chargerId) return;

    io.to(chargerId).emit('evse-scheduler', evt);
    io.to(chargerId).emit('scheduler-state', evt);
  });

  mqttClient.on('loadbalancing-state', (evt) => {
    const chargerId = evt?.charger_id;
    if (!chargerId) return;

    io.to(chargerId).emit('evse-loadbalancing', evt);
    io.to(chargerId).emit('loadbalancing-state', evt);
  });

  mqttClient.on('meter-live', (evt) => {
    const chargerId = evt?.charger_id;
    if (!chargerId) return;
    io.to(chargerId).emit('meter-live', evt);
  });

  mqttClient.on('auth-state', (evt) => {
    const chargerId = evt?.charger_id;
    if (!chargerId) return;
    io.to(chargerId).emit('auth-state', evt);
  });

  mqttClient.on('meters-config', (evt) => {
    const chargerId = evt?.charger_id;
    if (!chargerId) return;
    io.to(chargerId).emit('meters-config', evt);
  });
}

server.listen(config.port, () => {
  console.log(`Server listening on http://localhost:${config.port}`);
});

// === Fim de: ./src/server.js ===


// === Início de: ./src/app.js ===
// src/app.js
const express = require('express');

const config = require('./config');

const usersRouter = require('./routes/users.routes');
const chargersRouter = require('./routes/chargers.routes');
const sessionsRouter = require('./routes/sessions.routes');
const pushRouter = require('./routes/push.routes');

const errorHandler = require('./middleware/errorHandler');

const app = express();

// body parser
app.use(express.json({ limit: '1mb' }));

// CORS simples sem dependência extra
app.use((req, res, next) => {
  const origin = req.headers.origin;
  if (origin && config.corsOrigins.includes(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin);
    res.setHeader('Vary', 'Origin');
    res.setHeader('Access-Control-Allow-Credentials', 'true');
  }

  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS');

  if (req.method === 'OPTIONS') return res.sendStatus(204);
  next();
});

// health
app.get('/health', (req, res) => res.json({ ok: true }));

// routes
app.use('/api/users', usersRouter);
app.use('/api/chargers', chargersRouter);
app.use('/api/charger_sessions', sessionsRouter);
app.use('/api/push', pushRouter);

// 404
app.use((req, res) => {
  res.status(404).json({ success: false, message: 'Not found' });
});

// error handler
app.use(errorHandler);

module.exports = app;

// === Fim de: ./src/app.js ===


// === Início de: ./src/services/push.service.js ===
// src/services/push.service.js
const webpush = require('web-push');
const config = require('../config');
const pushRepo = require('../repositories/push.repo');

const hasVapid = !!config.vapid.publicKey && !!config.vapid.privateKey;

if (!hasVapid) {
  console.warn('[Push] VAPID keys não definidas. Push desativado.');
} else {
  webpush.setVapidDetails(config.vapid.subject, config.vapid.publicKey, config.vapid.privateKey);
}

async function sendWithRetry(subscription, message, tries = 2) {
  try {
    return await webpush.sendNotification(subscription, message);
  } catch (err) {
    const code = err?.statusCode;
    if ((code === 429 || code >= 500) && tries > 1) {
      await new Promise((r) => setTimeout(r, 1000));
      return sendWithRetry(subscription, message, tries - 1);
    }
    throw err;
  }
}

async function sendPushToUser(userId, payload) {
  if (!hasVapid) return;

  const subs = await pushRepo.listByUser(userId);
  if (!subs.length) return;

  const message = JSON.stringify(payload);

  await Promise.allSettled(
    subs.map(async (s) => {
      const subscription = {
        endpoint: s.endpoint,
        keys: { p256dh: s.p256dh, auth: s.auth },
      };

      try {
        await sendWithRetry(subscription, message);
      } catch (err) {
        const code = err?.statusCode;
        if (code === 404 || code === 410) {
          await pushRepo.deleteById(s.id);
        } else {
          console.error('[Push] erro ao enviar:', err.message);
        }
      }
    })
  );
}

module.exports = { sendPushToUser };

// === Fim de: ./src/services/push.service.js ===


// === Início de: ./src/services/sessions.service.js ===
// src/services/sessions.service.js
const chargersRepo = require('../repositories/chargers.repo');
const sessionsRepo = require('../repositories/sessions.repo');
const { httpError } = require('../utils/httpError');

function stripUndef(obj) {
  return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined));
}

async function listByCharger(userId, chargerId) {
  const charger = await chargersRepo.findByIdForUser(chargerId, userId);
  if (!charger) throw httpError(403, 'Acesso não autorizado');

  return sessionsRepo.listByCharger(chargerId);
}

async function history(userId, chargerId, viewMode) {
  const charger = await chargersRepo.findByIdForUser(chargerId, userId);
  if (!charger) throw httpError(403, 'Acesso não autorizado');

  const rows = await sessionsRepo.historyAgg(chargerId, viewMode);
  if (!rows.length) return [];

  return rows.map((r) => ({
    started_at: r.period,
    kwh: parseFloat(r.total_kwh) || 0,
  }));
}

async function getById(userId, sessionId) {
  const session = await sessionsRepo.findByIdForUser(sessionId, userId);
  if (!session) throw httpError(404, 'Sessão não encontrada');
  return session;
}

async function create(userId, charger_id) {
  const charger = await chargersRepo.findByIdForUser(charger_id, userId);
  if (!charger) throw httpError(403, 'Acesso não autorizado');

  return sessionsRepo.insertSession({ charger_id, started_at: new Date() });
}

async function update(userId, sessionId, patch) {
  const session = await sessionsRepo.findByIdForUser(sessionId, userId);
  if (!session) throw httpError(404, 'Sessão não encontrada');

  const clean = stripUndef(patch);
  return sessionsRepo.updateById(sessionId, clean);
}

async function remove(userId, sessionId) {
  const deleted = await sessionsRepo.deleteByIdForUser(sessionId, userId);
  if (!deleted) throw httpError(404, 'Sessão não encontrada');
  return true;
}

module.exports = { listByCharger, history, getById, create, update, remove };

// === Fim de: ./src/services/sessions.service.js ===


// === Início de: ./src/services/pushHttp.service.js ===
// src/services/pushHttp.service.js
const pushRepo = require('../repositories/push.repo');
const { httpError } = require('../utils/httpError');

async function subscribe(userId, endpoint, keys, userAgent) {
  if (!endpoint || !keys?.p256dh || !keys?.auth) {
    throw httpError(400, 'Subscription inválida');
  }

  // dedupe (mesmo user)
  const existing = await pushRepo.findByUserAndEndpoint(userId, endpoint);
  if (existing) return { row: existing, created: false };

  // como endpoint é UNIQUE na tabela, evita conflito com outro user
  const usedByOther = await pushRepo.findByEndpoint(endpoint);
  if (usedByOther && usedByOther.user_id !== userId) {
    throw httpError(409, 'Este endpoint já está associado a outro utilizador');
  }

  const inserted = await pushRepo.insertSubscription({
    user_id: userId,
    endpoint,
    p256dh: keys.p256dh,
    auth: keys.auth,
    user_agent: userAgent || null,
    created_at: new Date().toISOString(),
  });

  return { row: inserted, created: true };
}

async function unsubscribe(userId, endpoint) {
  if (!endpoint) return { ok: true, message: 'No subscription' };
  await pushRepo.deleteByUserAndEndpoint(userId, endpoint);
  return { ok: true, message: 'Unsubscribed' };
}

module.exports = { subscribe, unsubscribe };

// === Fim de: ./src/services/pushHttp.service.js ===


// === Início de: ./src/services/configs.service.js ===

// === Fim de: ./src/services/configs.service.js ===


// === Início de: ./src/services/chargers.service.js ===
// src/services/chargers.service.js
const crypto = require('crypto');
const axios = require('axios');

const chargersRepo = require('../repositories/chargers.repo');
const { httpError } = require('../utils/httpError');
const mqttClient = require('../mqtt');

function stripUndef(obj) {
  return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined));
}

// throttle in-memory (por charger)
const lastConfigUpdateAt = new Map();

function clampAmp(v) {
  const n = Number(v);
  return Math.max(6, Math.min(n, 64));
}

function clampTemp(v) {
  const n = Number(v);
  return Math.max(0, Math.min(n, 120));
}

function normalizeNumericFields(charger) {
  const numericFields = [
    'power_l1',
    'power_l2',
    'power_l3',
    'voltage_l1',
    'voltage_l2',
    'voltage_l3',
    'current_l1',
    'current_l2',
    'current_l3',
    'charging_current',
    'consumption',
  ];

  numericFields.forEach((field) => {
    const v = charger[field];
    charger[field] = v === null || v === undefined || v === '' ? 0 : Number(v);
    if (Number.isNaN(charger[field])) charger[field] = 0;
  });

  return charger;
}

async function list(userId) {
  return chargersRepo.listByUser(userId);
}

async function getOne(userId, id) {
  const charger = await chargersRepo.findByIdForUser(id, userId);
  if (!charger) throw httpError(404, 'Carregador não encontrado');

  let config = await chargersRepo.getConfig(charger.id);
  if (!config) {
    config = {
      charger_id: charger.id,
      max_charging_current: 32,
      require_auth: false,
      rcm_enabled: false,
      temperature_limit: 60,
    };
  }

  return { ...normalizeNumericFields(charger), config };
}

async function create(userId, location) {
  if (!location || typeof location !== 'string' || location.trim().length < 1) {
    throw httpError(400, 'O campo location é obrigatório');
  }

  const now = new Date().toISOString();

  let chargerID;
  do {
    chargerID = crypto.randomBytes(6).toString('hex');
  } while (await chargersRepo.findByMqttTopic(chargerID));

  const mqtt_topic = chargerID;
  const mqtt_user = chargerID;
  const mqtt_pass = crypto.randomBytes(6).toString('hex');

  const charger = await chargersRepo.insertCharger({
    user_id: userId,
    location: location.trim(),
    status: 'offline',
    charging_current: 0,
    charging_time: 0,
    consumption: 0,
    power_l1: 0.0,
    power_l2: 0.0,
    power_l3: 0.0,
    voltage_l1: 0.0,
    voltage_l2: 0.0,
    voltage_l3: 0.0,
    current_l1: 0.0,
    current_l2: 0.0,
    current_l3: 0.0,
    mqtt_user,
    mqtt_pass,
    mqtt_topic,
    updated_at: now,
  });

  await chargersRepo.insertConfig({
    charger_id: charger.id,
    max_charging_current: 32,
    require_auth: false,
    rcm_enabled: false,
    temperature_limit: 60,
    config_received_at: now,
  });

  // mosquitto mgmt (mantém como tinhas)
  try {
    await axios.post(
      'http://localhost:7000/client/create',
      {
        client_name: charger.mqtt_user,
        chargeID: charger.mqtt_topic,
        password: charger.mqtt_pass,
      },
      { timeout: 5000 }
    );
  } catch (err) {
    console.error('Erro ao criar cliente Mosquitto:', err?.response?.data || err.message);
  }

  return charger;
}

async function update(userId, id, payload = {}) {
  let { charger = {}, config = {} } = payload;

  // compat: { location } no root
  if (payload.location && !charger.location) charger.location = payload.location;

  // só permitimos location por agora
  const safeChargerUpdate = {};
  if (charger.location !== undefined) safeChargerUpdate.location = charger.location;

  let updatedCharger = null;

  if (Object.keys(safeChargerUpdate).length > 0) {
    updatedCharger = await chargersRepo.updateChargerForUser(id, userId, {
      ...safeChargerUpdate,
      updated_at: new Date().toISOString(),
    });
  } else {
    updatedCharger = await chargersRepo.findByIdForUser(id, userId);
  }

  if (!updatedCharger) throw httpError(404, 'Carregador não encontrado');

  // ✅ config patch com whitelist (para evitar lixo no DB)
  if (config && Object.keys(config).length > 0) {
    const ALLOWED = ['max_charging_current', 'require_auth', 'rcm_enabled', 'temperature_limit'];
    let safeConfig = Object.fromEntries(
      Object.entries(config || {}).filter(([k]) => ALLOWED.includes(k))
    );

    // clamp leve (evita valores inválidos no DB)
    if (safeConfig.max_charging_current !== undefined) {
      safeConfig.max_charging_current = clampAmp(safeConfig.max_charging_current);
    }
    if (safeConfig.temperature_limit !== undefined) {
      safeConfig.temperature_limit = clampTemp(safeConfig.temperature_limit);
    }

    await chargersRepo.upsertConfig(id, {
      ...stripUndef(safeConfig),
      config_received_at: new Date().toISOString(),
    });
  }

  return updatedCharger;
}

async function remove(userId, id) {
  const charger = await chargersRepo.findByIdForUser(id, userId);
  if (!charger) throw httpError(404, 'Carregador não encontrado');

  try {
    await axios.post(
      'http://localhost:7000/client/delete',
      {
        client_name: charger.mqtt_user,
        chargeID: charger.mqtt_topic,
      },
      { timeout: 5000 }
    );
  } catch (err) {
    console.error('Erro ao deletar cliente Mosquitto:', err?.response?.data || err.message);
  }

  await chargersRepo.deleteChargerForUser(id, userId);
  return true;
}

async function updateConfig(userId, id, incomingConfig = {}) {
  const charger = await chargersRepo.findByIdForUser(id, userId);
  if (!charger) throw httpError(404, 'Charger not found or unauthorized');

  const existing = await chargersRepo.getConfig(id);

  const nowMs = Date.now();
  const lastMs = lastConfigUpdateAt.get(id) || 0;
  const tooSoon = nowMs - lastMs < 800;

  const ALLOWED = ['max_charging_current', 'require_auth', 'rcm_enabled', 'temperature_limit'];
  let safeConfig = Object.fromEntries(
    Object.entries(incomingConfig || {}).filter(([k]) => ALLOWED.includes(k))
  );

  if (safeConfig.max_charging_current !== undefined) {
    safeConfig.max_charging_current = clampAmp(safeConfig.max_charging_current);
  }
  if (safeConfig.temperature_limit !== undefined) {
    safeConfig.temperature_limit = clampTemp(safeConfig.temperature_limit);
  }

  const onlyAmp =
    Object.keys(safeConfig).length === 1 && safeConfig.max_charging_current !== undefined;

  if (
    existing &&
    onlyAmp &&
    Number(existing.max_charging_current) === Number(safeConfig.max_charging_current)
  ) {
    return { data: existing, message: 'Config unchanged' };
  }

  if (tooSoon && existing && onlyAmp) {
    return { data: existing, message: 'Throttled' };
  }

  const updated = await chargersRepo.upsertConfig(id, {
    ...safeConfig,
    config_received_at: new Date().toISOString(),
  });

  lastConfigUpdateAt.set(id, nowMs);

  // publica p/ firmware novo: cmd/evse/settings
  const evseSettings = {};
  if (safeConfig.max_charging_current !== undefined) {
    evseSettings.currentLimit = Number(safeConfig.max_charging_current);
  }
  if (safeConfig.temperature_limit !== undefined) {
    evseSettings.temperatureLimit = Number(safeConfig.temperature_limit);
  }
  if (Object.keys(evseSettings).length > 0) {
    mqttClient.sendEvseSettings(charger.mqtt_topic, evseSettings);
  }

  return { data: updated };
}

async function getSchedules(userId, id) {
  const charger = await chargersRepo.findByIdForUser(id, userId);
  if (!charger) throw httpError(404, 'Carregador não encontrado');
  return chargersRepo.listSchedules(id);
}

async function createSchedule(userId, id, start, end, repeat) {
  const charger = await chargersRepo.findByIdForUser(id, userId);
  if (!charger) throw httpError(404, 'Carregador não encontrado');

  const row = await chargersRepo.insertSchedule({
    charger_id: id,
    start,
    end,
    repeat,
    created_at: new Date().toISOString(),
  });

  return row;
}

async function action(userId, id, actionName, ampLimit) {
  const charger = await chargersRepo.findByIdForUser(id, userId);
  if (!charger) throw httpError(404, 'Carregador não encontrado ou não autorizado');

  if (ampLimit !== undefined) {
    const safeAmp = clampAmp(ampLimit);

    await chargersRepo.upsertConfig(id, {
      max_charging_current: safeAmp,
      config_received_at: new Date().toISOString(),
    });

    mqttClient.sendEvseSettings(charger.mqtt_topic, { currentLimit: safeAmp });
  }

  const enable = actionName === 'start';
  mqttClient.sendEnable(charger.mqtt_topic, enable);

  return true;
}

module.exports = {
  list,
  getOne,
  create,
  update,
  remove,
  updateConfig,
  getSchedules,
  createSchedule,
  action,
};

// === Fim de: ./src/services/chargers.service.js ===


// === Início de: ./src/services/users.service.js ===
// src/services/users.service.js
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const config = require('../config');
const usersRepo = require('../repositories/users.repo');
const { httpError } = require('../utils/httpError');

async function login(username, password) {
    if (!username || !password) {
        throw httpError(400, 'Usuário e senha são obrigatórios');
    }

    const user = await usersRepo.findByUsername(username);
    if (!user) throw httpError(401, 'Credenciais inválidas');

    const ok = await bcrypt.compare(password, user.password);
    if (!ok) throw httpError(401, 'Credenciais inválidas');

    const token = jwt.sign({ id: user.id, username: user.username }, config.jwtSecret, {
        expiresIn: '24h',
    });

    return { token };
}

async function register(username, password) {
    if (
        !username ||
        !password ||
        typeof username !== 'string' ||
        typeof password !== 'string' ||
        username.length < 3 ||
        password.length < 4
    ) {
        throw httpError(
            400,
            'Nome de usuário deve ter pelo menos 3 caracteres e senha pelo menos 4 caracteres'
        );
    }

    const existing = await usersRepo.findByUsername(username);
    if (existing) throw httpError(409, 'Nome de usuário já está em uso');

    const hashed = await bcrypt.hash(password, 10);
    const id = await usersRepo.insertUser({ username, passwordHash: hashed });

    const token = jwt.sign({ id, username }, config.jwtSecret, { expiresIn: '24h' });
    return { token };
}

module.exports = { login, register };

// === Fim de: ./src/services/users.service.js ===


// === Início de: ./src/utils/httpError.js ===
// src/utils/httpError.js
class HttpError extends Error {
    constructor(statusCode, message) {
        super(message);
        this.statusCode = statusCode;
    }
}

function httpError(statusCode, message) {
    return new HttpError(statusCode, message);
}

module.exports = { HttpError, httpError };

// === Fim de: ./src/utils/httpError.js ===


// === Início de: ./src/middleware/auth.js ===
// src/middleware/auth.js
const jwt = require('jsonwebtoken');
const config = require('../config');

function verifyToken(req, res, next) {
  const authHeader = req.headers['authorization'] || req.headers['Authorization'];

  if (!authHeader) {
    return res.status(401).json({ error: 'Token não fornecido' });
  }

  const match = authHeader.match(/^Bearer\s+(.+)$/i);
  if (!match) {
    return res.status(401).json({ error: 'Token malformado. Use "Bearer <token>"' });
  }

  const token = match[1];

  jwt.verify(token, config.jwtSecret, (err, payload) => {
    if (err) {
      if (err.name === 'TokenExpiredError') {
        return res.status(401).json({ error: 'Sessão expirada' });
      }
      return res.status(401).json({ error: 'Token inválido' });
    }

    if (!payload?.id) {
      return res.status(401).json({ error: 'Token inválido' });
    }

    req.user = payload;
    next();
  });
}

module.exports = verifyToken;

// === Fim de: ./src/middleware/auth.js ===


// === Início de: ./src/middleware/validate.js ===
// src/middleware/validate.js
const { validationResult } = require('express-validator');

function handleValidation(req, res, next) {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({ success: false, errors: errors.array() });
  }
  next();
}

module.exports = handleValidation;

// === Fim de: ./src/middleware/validate.js ===


// === Início de: ./src/middleware/errorHandler.js ===
// src/middleware/errorHandler.js
function errorHandler(err, req, res, next) {
    console.error('[errorHandler]', err);
  
    if (res.headersSent) return next(err);
  
    const status = err.statusCode || err.status || 500;
    const message = err.message || 'Erro interno do servidor';
  
    res.status(status).json({ success: false, message });
  }
  
  module.exports = errorHandler;
  
// === Fim de: ./src/middleware/errorHandler.js ===


// === Início de: ./src/db/knex.js ===
// src/db/knex.js
const knex = require('knex');
const path = require('path');

const knexfile = require(path.join(__dirname, '../../knexfile.js'));
const env = process.env.NODE_ENV || 'development';

const db = knex(knexfile[env] || knexfile);

module.exports = db;

// === Fim de: ./src/db/knex.js ===


// === Início de: ./src/db/migrations/20251123_create_charger_schedules.js ===
exports.up = async function (knex) {
  const exists = await knex.schema.hasTable('charger_schedules');
  if (exists) return;

  return knex.schema.createTable('charger_schedules', (t) => {
    t.uuid('id')
      .primary()
      .defaultTo(knex.raw('gen_random_uuid()'));

    t.uuid('charger_id')
      .notNullable()
      .references('id')
      .inTable('chargers')
      .onDelete('CASCADE');

    t.string('start', 5).notNullable();
    t.string('end', 5).notNullable();

    t.enu('repeat', ['everyday', 'weekdays', 'weekends'])
      .notNullable()
      .defaultTo('everyday');

    t.timestamp('created_at').defaultTo(knex.fn.now());
  });
};

exports.down = function (knex) {
  return knex.schema.dropTableIfExists('charger_schedules');
};

// === Fim de: ./src/db/migrations/20251123_create_charger_schedules.js ===


// === Início de: ./src/db/migrations/20251123_create_push_subscriptions.js ===
exports.up = function (knex) {
    return knex.schema.createTable('push_subscriptions', (t) => {
      t.uuid('id')
        .primary()
        .defaultTo(knex.raw('gen_random_uuid()'));
  
      // ✅ users.id é integer no teu caso
      t.integer('user_id')
        .notNullable()
        .references('id')
        .inTable('users')
        .onDelete('CASCADE');
  
      t.text('endpoint').notNullable().unique();
      t.text('p256dh').notNullable();
      t.text('auth').notNullable();
      t.text('user_agent');
      t.timestamp('created_at').defaultTo(knex.fn.now());
    });
  };
  
  exports.down = function (knex) {
    return knex.schema.dropTableIfExists('push_subscriptions');
  };
  
// === Fim de: ./src/db/migrations/20251123_create_push_subscriptions.js ===


// === Início de: ./src/db/migrations/20250619_create_tables.js ===
// migrations/20250619_create_tables.js
exports.up = async function(knex) {
  // Create 'users' table
  await knex.schema.createTable('users', (table) => {
    table.increments('id').primary();
    table.string('username', 255).notNullable().unique();
    table.string('password', 255).notNullable();
    table.timestamp('created_at').defaultTo(knex.fn.now());
  });

  // Create 'chargers' table with new fields
  await knex.schema.createTable('chargers', (table) => {
    table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
    table.integer('user_id').unsigned().notNullable()
         .references('id').inTable('users').onDelete('CASCADE');
    table.string('location', 255).notNullable();
    table.string('status', 50).notNullable().defaultTo('offline');
    table.integer('charging_current').notNullable().defaultTo(32);
    table.timestamp('updated_at').notNullable().defaultTo(knex.fn.now());
    table.string('mqtt_user', 255).notNullable();
    table.string('mqtt_pass', 255).notNullable();
    table.string('mqtt_topic', 255).notNullable().unique();
    table.timestamp('created_at').defaultTo(knex.fn.now());

    // Add power and current for 3 phases, voltage and other new fields
    table.integer('charging_time').notNullable().defaultTo(0); // Total charging time
    table.decimal('consumption', 8, 2).notNullable().defaultTo(0); // Consumption (kWh)
    
    // Power for 3 phases (L1, L2, L3)
    table.decimal('power_l1', 8, 2).notNullable().defaultTo(0);
    table.decimal('power_l2', 8, 2).notNullable().defaultTo(0);
    table.decimal('power_l3', 8, 2).notNullable().defaultTo(0);

    // Voltage for 3 phases (L1, L2, L3)
    table.decimal('voltage_l1', 8, 2).notNullable().defaultTo(0);
    table.decimal('voltage_l2', 8, 2).notNullable().defaultTo(0);
    table.decimal('voltage_l3', 8, 2).notNullable().defaultTo(0);

    // Current for 3 phases (L1, L2, L3)
    table.decimal('current_l1', 8, 2).notNullable().defaultTo(0);
    table.decimal('current_l2', 8, 2).notNullable().defaultTo(0);
    table.decimal('current_l3', 8, 2).notNullable().defaultTo(0);
  });

  // Create 'charger_configs' table
  await knex.schema.createTable('charger_configs', (table) => {
    table.uuid('charger_id').primary()
         .references('id').inTable('chargers').onDelete('CASCADE');
    table.integer('max_charging_current').notNullable().defaultTo(32);
    table.boolean('require_auth').notNullable().defaultTo(false);
    table.boolean('rcm_enabled').notNullable().defaultTo(false);
    table.integer('temperature_limit').notNullable().defaultTo(60);
    table.timestamp('config_received_at').notNullable().defaultTo(knex.fn.now());
  });

  // Create 'charger_sessions' table
  await knex.schema.createTable('charger_sessions', (table) => {
    table.increments('id').primary();
    table.uuid('charger_id').notNullable()
         .references('id').inTable('chargers').onDelete('CASCADE');
    table.timestamp('started_at').notNullable();
    table.timestamp('ended_at');
    table.decimal('kwh', 8, 2).notNullable().defaultTo(0);
    table.decimal('cost', 10, 2);
    table.timestamp('created_at').defaultTo(knex.fn.now());
  });
};

exports.down = async function(knex) {
  await knex.schema.dropTableIfExists('charger_sessions');
  await knex.schema.dropTableIfExists('charger_configs');
  await knex.schema.dropTableIfExists('chargers');
  await knex.schema.dropTableIfExists('users');
};

// === Fim de: ./src/db/migrations/20250619_create_tables.js ===


// === Início de: ./src/db/migrations/20250618_enable_pgcrypto.js ===
exports.up = async function (knex) {
    await knex.raw('CREATE EXTENSION IF NOT EXISTS pgcrypto');
  };
  
  exports.down = async function (knex) {
    // normalmente não se remove extensão em down, mas deixo seguro:
    // await knex.raw('DROP EXTENSION IF EXISTS pgcrypto');
  };
  
// === Fim de: ./src/db/migrations/20250618_enable_pgcrypto.js ===


// === Início de: ./src/db/migrations/20251123084023_create_charger_schedules.js ===
// shim para compatibilidade com o nome antigo registado no knex_migrations
module.exports = require('./20251123_create_charger_schedules');

// === Fim de: ./src/db/migrations/20251123084023_create_charger_schedules.js ===


// === Início de: ./src/domain/normalize/chargingConfig.js ===
// src/domain/normalize/chargingConfig.js

/**
 * Normaliza eventos de config (quando o carregador manda config)
 */
function normalizeChargingConfig(data = {}) {
    const chargerId = data.charger_id || data.chargerId || data.id;
    const cfg = data.config || data.raw?.config || data;

    return {
        charger_id: chargerId,
        mqtt_topic: data.mqtt_topic || data.mqttTopic,
        config: {
            max_charging_current:
                cfg.max_charging_current ??
                cfg.maxChargingCurrent ??
                cfg.max_current ??
                cfg.maxCurrent ??
                undefined,

            require_auth: cfg.require_auth ?? cfg.requireAuth ?? undefined,
            rcm_enabled: cfg.rcm_enabled ?? cfg.rcmEnabled ?? undefined,
            temperature_limit:
                cfg.temperature_limit ??
                cfg.temperatureThreshold ??
                cfg.temp_limit ??
                undefined,
        },
        raw: data.raw || data,
        updated_at: new Date().toISOString(),
    };
}

module.exports = { normalizeChargingConfig };

// === Fim de: ./src/domain/normalize/chargingConfig.js ===


// === Início de: ./src/domain/normalize/chargingStatus.js ===
// src/domain/normalize/chargingStatus.js

const toNum = (v) => {
    if (v === null || v === undefined || v === '') return 0;
    const n = typeof v === 'number' ? v : parseFloat(v);
    return Number.isFinite(n) ? n : 0;
};

const toArr3 = (v) => {
    if (Array.isArray(v)) return [toNum(v[0]), toNum(v[1]), toNum(v[2])];
    if (v && typeof v === 'object') return [toNum(v.l1), toNum(v.l2), toNum(v.l3)];
    return [0, 0, 0];
};

const normalizeStatusText = (rawStatus) => {
    const s = String(rawStatus || '').toLowerCase();

    if (s.includes('charging')) return '⚡ Charging';
    if (s.includes('ready')) return '🟢 Ready';
    if (s.includes('fault') || s.includes('error')) return '⚠️ Fault';
    if (s.includes('wait')) return '⚡ Wait';
    if (s.includes('not conn') || s.includes('disconnected')) return '🔌 Not Conn.';
    if (s.includes('vent')) return '💨 Vent';

    return rawStatus || '—';
};

/**
 * Normaliza eventos de status (realtime) vindos do mqtt -> socket
 */
function normalizeChargingStatus(data = {}) {
    const chargerId = data.charger_id || data.chargerId || data.id;

    const powerArr = toArr3(data.power || data.raw?.power);
    const voltageArr = toArr3(data.voltage || data.raw?.voltage);
    const currentArr = toArr3(data.current || data.raw?.current);

    const status = normalizeStatusText(data.status || data.state || data.raw?.state);

    const chargingTime =
        toNum(data.charging_time) ||
        toNum(data.chargingTime) ||
        toNum(data.raw?.chargingTime) ||
        toNum(data.raw?.sessionTime);

    const consumption = toNum(data.consumption) || toNum(data.raw?.consumption);

    const chargingCurrent =
        toNum(data.charging_current) ||
        toNum(data.chargingCurrent) ||
        currentArr[0];

    return {
        charger_id: chargerId,
        mqtt_topic: data.mqtt_topic || data.mqttTopic,

        status,
        stateCode: data.stateCode || data.raw?.stateCode || undefined,

        consumption,
        charging_time: chargingTime,
        charging_current: chargingCurrent,

        power: powerArr,
        voltage: voltageArr,
        current: currentArr,

        raw: data.raw || data,
        updated_at: new Date().toISOString(),
    };
}

module.exports = { normalizeChargingStatus };

// === Fim de: ./src/domain/normalize/chargingStatus.js ===


// === Início de: ./src/routes/chargers.routes.js ===
// src/routes/chargers.routes.js
const express = require('express');
const { body, param } = require('express-validator');

const verifyToken = require('../middleware/auth');
const handleValidation = require('../middleware/validate');
const chargersService = require('../services/chargers.service');

const router = express.Router();
router.use(verifyToken);

router.get('/', async (req, res, next) => {
  try {
    const data = await chargersService.list(req.user.id);
    res.json({ success: true, data });
  } catch (err) {
    next(err);
  }
});

router.get(
  '/:id',
  [param('id').isUUID()],
  handleValidation,
  async (req, res, next) => {
    try {
      const data = await chargersService.getOne(req.user.id, req.params.id);
      res.json({ success: true, data });
    } catch (err) {
      next(err);
    }
  }
);

router.post(
  '/',
  [body('location').exists().isString().isLength({ min: 1 }).trim()],
  handleValidation,
  async (req, res, next) => {
    try {
      const data = await chargersService.create(req.user.id, req.body.location);
      res.status(201).json({ success: true, data });
    } catch (err) {
      next(err);
    }
  }
);

router.put(
  '/:id',
  [
    param('id').isUUID(),
    body('charger').optional().isObject(),
    body('config').optional().isObject(),
    body('location').optional().isString(),
  ],
  handleValidation,
  async (req, res, next) => {
    try {
      const data = await chargersService.update(req.user.id, req.params.id, req.body);
      res.json({ success: true, data });
    } catch (err) {
      next(err);
    }
  }
);

router.delete(
  '/:id',
  [param('id').isUUID()],
  handleValidation,
  async (req, res, next) => {
    try {
      await chargersService.remove(req.user.id, req.params.id);
      res.json({ success: true, message: 'Carregador excluído com sucesso' });
    } catch (err) {
      next(err);
    }
  }
);

router.put(
  '/:id/config',
  [param('id').isUUID(), body('config').isObject()],
  handleValidation,
  async (req, res, next) => {
    try {
      const out = await chargersService.updateConfig(req.user.id, req.params.id, req.body.config);
      res.json({ success: true, data: out.data, message: out.message });
    } catch (err) {
      next(err);
    }
  }
);

router.get(
  '/:id/schedule',
  [param('id').isUUID()],
  handleValidation,
  async (req, res, next) => {
    try {
      const data = await chargersService.getSchedules(req.user.id, req.params.id);
      res.json({ success: true, data });
    } catch (err) {
      next(err);
    }
  }
);

router.post(
  '/:id/schedule',
  [
    param('id').isUUID(),
    body('start').matches(/^\d{2}:\d{2}$/),
    body('end').matches(/^\d{2}:\d{2}$/),
    body('repeat').isIn(['everyday', 'weekdays', 'weekends']),
  ],
  handleValidation,
  async (req, res, next) => {
    try {
      const { start, end, repeat } = req.body;
      const data = await chargersService.createSchedule(req.user.id, req.params.id, start, end, repeat);
      res.status(201).json({ success: true, data });
    } catch (err) {
      next(err);
    }
  }
);

router.post(
  '/:id/action',
  [
    param('id').isUUID(),
    body('action').isIn(['start', 'stop']),
    body('ampLimit').optional().isInt({ min: 6, max: 64 }),
  ],
  handleValidation,
  async (req, res, next) => {
    try {
      const { action, ampLimit } = req.body;
      await chargersService.action(req.user.id, req.params.id, action, ampLimit);
      res.json({ success: true, message: `Comando '${action}' enviado com sucesso` });
    } catch (err) {
      next(err);
    }
  }
);

module.exports = router;

// === Fim de: ./src/routes/chargers.routes.js ===


// === Início de: ./src/routes/sessions.routes.js ===
// src/routes/sessions.routes.js
const express = require('express');
const { param, query, body } = require('express-validator');

const verifyToken = require('../middleware/auth');
const handleValidation = require('../middleware/validate');
const sessionsService = require('../services/sessions.service');

const router = express.Router();
router.use(verifyToken);

// GET /api/charger_sessions?chargerId=...
router.get(
  '/',
  [query('chargerId').isUUID().withMessage('chargerId deve ser UUID válido')],
  handleValidation,
  async (req, res, next) => {
    try {
      const { chargerId } = req.query;
      const data = await sessionsService.listByCharger(req.user.id, chargerId);
      res.json({ success: true, data });
    } catch (err) {
      next(err);
    }
  }
);

// ✅ /history antes de /:id
router.get(
  '/history/:chargerId',
  [
    param('chargerId').isUUID().withMessage('chargerId deve ser UUID válido'),
    query('viewMode').isIn(['Day', 'Week', 'Month']).withMessage('viewMode inválido'),
  ],
  handleValidation,
  async (req, res, next) => {
    try {
      const { chargerId } = req.params;
      const { viewMode } = req.query;

      const data = await sessionsService.history(req.user.id, chargerId, viewMode);
      res.json({ success: true, data });
    } catch (err) {
      next(err);
    }
  }
);

router.get(
  '/:id',
  [param('id').isInt().withMessage('ID de sessão inválido')],
  handleValidation,
  async (req, res, next) => {
    try {
      const data = await sessionsService.getById(req.user.id, Number(req.params.id));
      res.json({ success: true, data });
    } catch (err) {
      next(err);
    }
  }
);

router.post(
  '/',
  [body('charger_id').isUUID().withMessage('charger_id deve ser UUID válido')],
  handleValidation,
  async (req, res, next) => {
    try {
      const data = await sessionsService.create(req.user.id, req.body.charger_id);
      res.status(201).json({ success: true, data });
    } catch (err) {
      next(err);
    }
  }
);

router.put(
  '/:id',
  [
    param('id').isInt().withMessage('ID de sessão inválido'),
    body('ended_at').optional().isISO8601().toDate(),
    body('kwh').optional().isFloat({ min: 0 }),
    body('cost').optional().isFloat({ min: 0 }),
  ],
  handleValidation,
  async (req, res, next) => {
    try {
      const id = Number(req.params.id);
      const { ended_at, kwh, cost } = req.body;

      const data = await sessionsService.update(req.user.id, id, { ended_at, kwh, cost });
      res.json({ success: true, data });
    } catch (err) {
      next(err);
    }
  }
);

router.delete(
  '/:id',
  [param('id').isInt().withMessage('ID de sessão inválido')],
  handleValidation,
  async (req, res, next) => {
    try {
      const ok = await sessionsService.remove(req.user.id, Number(req.params.id));
      res.json({ success: true, message: ok ? 'Sessão excluída com sucesso' : 'OK' });
    } catch (err) {
      next(err);
    }
  }
);

module.exports = router;

// === Fim de: ./src/routes/sessions.routes.js ===


// === Início de: ./src/routes/push.routes.js ===
// src/routes/push.routes.js
const express = require('express');
const { body } = require('express-validator');

const verifyToken = require('../middleware/auth');
const handleValidation = require('../middleware/validate');
const config = require('../config');

const pushHttpService = require('../services/pushHttp.service');
const { sendPushToUser } = require('../services/push.service');

const router = express.Router();
router.use(verifyToken);

// GET /api/push/vapid-public-key
router.get('/vapid-public-key', (req, res) => {
  if (!config.vapid.publicKey) {
    return res.status(503).json({ success: false, message: 'Push indisponível' });
  }
  res.json({ success: true, data: { key: config.vapid.publicKey } });
});

// POST /api/push/subscribe
router.post(
  '/subscribe',
  [
    body('endpoint').isString(),
    body('keys.p256dh').isString(),
    body('keys.auth').isString(),
  ],
  handleValidation,
  async (req, res, next) => {
    try {
      const userId = req.user.id;
      const { endpoint, keys } = req.body;
      const ua = req.headers['user-agent'] || null;

      const out = await pushHttpService.subscribe(userId, endpoint, keys, ua);
      res.status(out.created ? 201 : 200).json({ success: true, data: out.row });
    } catch (err) {
      next(err);
    }
  }
);

// POST /api/push/unsubscribe
router.post(
  '/unsubscribe',
  [body('endpoint').optional().isString()],
  handleValidation,
  async (req, res, next) => {
    try {
      const userId = req.user.id;
      const { endpoint } = req.body || {};
      const out = await pushHttpService.unsubscribe(userId, endpoint);
      res.json({ success: true, message: out.message });
    } catch (err) {
      next(err);
    }
  }
);

// POST /api/push/test
router.post('/test', async (req, res, next) => {
  try {
    const userId = req.user.id;
    await sendPushToUser(userId, {
      title: '📬 Teste EV Station',
      body: 'Push notifications estão a funcionar!',
      url: '/',
    });
    res.json({ success: true, message: 'Push enviado' });
  } catch (err) {
    next(err);
  }
});

module.exports = router;

// === Fim de: ./src/routes/push.routes.js ===


// === Início de: ./src/routes/users.routes.js ===
// src/routes/users.routes.js
const express = require('express');
const rateLimit = require('express-rate-limit');
const usersService = require('../services/users.service');

const router = express.Router();

const authLimiter = rateLimit({
  windowMs: 60 * 1000,
  max: 10,
  standardHeaders: true,
  legacyHeaders: false,
});

router.post('/login', authLimiter, async (req, res, next) => {
  try {
    const { username, password } = req.body;
    const data = await usersService.login(username, password);
    res.json({ success: true, data });
  } catch (err) {
    next(err);
  }
});

router.post('/register', authLimiter, async (req, res, next) => {
  try {
    const { username, password } = req.body;
    const data = await usersService.register(username, password);
    res.status(201).json({ success: true, data });
  } catch (err) {
    next(err);
  }
});

module.exports = router;

// === Fim de: ./src/routes/users.routes.js ===


// === Início de: ./src/repositories/chargers.repo.js ===
// src/repositories/chargers.repo.js
const db = require('../db/knex');

async function listByUser(userId) {
    return db('chargers').where({ user_id: userId }).select('*');
}

async function findByIdForUser(id, userId) {
    return db('chargers').where({ id, user_id: userId }).first();
}

async function findByMqttTopic(mqtt_topic) {
    return db('chargers').where({ mqtt_topic }).first();
}

async function insertCharger(data) {
    const [row] = await db('chargers').insert(data).returning('*');
    return row;
}

async function updateChargerForUser(id, userId, patch) {
    const [row] = await db('chargers')
        .where({ id, user_id: userId })
        .update(patch)
        .returning('*');
    return row;
}

async function deleteChargerForUser(id, userId) {
    // devolve o charger antes para poderes usar mqtt_user/pass/topic etc
    const charger = await findByIdForUser(id, userId);
    if (!charger) return null;

    await db('chargers').where({ id, user_id: userId }).del();
    return charger;
}

async function getConfig(charger_id) {
    return db('charger_configs').where({ charger_id }).first();
}

async function insertConfig(data) {
    const [row] = await db('charger_configs').insert(data).returning('*');
    return row;
}

async function updateConfig(charger_id, patch) {
    const [row] = await db('charger_configs')
        .where({ charger_id })
        .update(patch)
        .returning('*');
    return row;
}

async function upsertConfig(charger_id, patch) {
    const existing = await getConfig(charger_id);
    if (existing) return updateConfig(charger_id, patch);
    return insertConfig({ charger_id, ...patch });
}

async function listSchedules(charger_id) {
    return db('charger_schedules').where({ charger_id }).orderBy('created_at', 'desc');
}

async function insertSchedule(data) {
    const [row] = await db('charger_schedules').insert(data).returning('*');
    return row;
}

module.exports = {
    listByUser,
    findByIdForUser,
    findByMqttTopic,
    insertCharger,
    updateChargerForUser,
    deleteChargerForUser,
    getConfig,
    insertConfig,
    updateConfig,
    upsertConfig,
    listSchedules,
    insertSchedule,
};

// === Fim de: ./src/repositories/chargers.repo.js ===


// === Início de: ./src/repositories/push.repo.js ===
// src/repositories/push.repo.js
const db = require('../db/knex');

async function findByUserAndEndpoint(user_id, endpoint) {
    return db('push_subscriptions').where({ user_id, endpoint }).first();
}

async function findByEndpoint(endpoint) {
    return db('push_subscriptions').where({ endpoint }).first();
}

async function listByUser(user_id) {
    return db('push_subscriptions')
        .where({ user_id })
        .select('id', 'endpoint', 'p256dh', 'auth');
}

async function insertSubscription(data) {
    const [row] = await db('push_subscriptions').insert(data).returning('*');
    return row;
}

async function deleteByUserAndEndpoint(user_id, endpoint) {
    return db('push_subscriptions').where({ user_id, endpoint }).del();
}

async function deleteById(id) {
    return db('push_subscriptions').where({ id }).del();
}

module.exports = {
    findByUserAndEndpoint,
    findByEndpoint,
    listByUser,
    insertSubscription,
    deleteByUserAndEndpoint,
    deleteById,
};

// === Fim de: ./src/repositories/push.repo.js ===


// === Início de: ./src/repositories/sessions.repo.js ===
// src/repositories/sessions.repo.js
const db = require('../db/knex');

async function listByCharger(charger_id) {
    return db('charger_sessions')
        .where({ charger_id })
        .orderBy('started_at', 'desc');
}

async function findByIdForUser(id, userId) {
    return db('charger_sessions')
        .join('chargers', 'charger_sessions.charger_id', 'chargers.id')
        .where({ 'charger_sessions.id': id, 'chargers.user_id': userId })
        .select('charger_sessions.*')
        .first();
}

async function insertSession(data) {
    const [row] = await db('charger_sessions').insert(data).returning('*');
    return row;
}

async function updateById(id, patch) {
    const [row] = await db('charger_sessions').where({ id }).update(patch).returning('*');
    return row;
}

async function deleteByIdForUser(id, userId) {
    return db('charger_sessions')
        .join('chargers', 'charger_sessions.charger_id', 'chargers.id')
        .where({ 'charger_sessions.id': id, 'chargers.user_id': userId })
        .del();
}

async function historyAgg(chargerId, viewMode) {
    let qb = db('charger_sessions')
        .where({ charger_id: chargerId })
        .sum('kwh as total_kwh');

    switch (viewMode) {
        case 'Day':
            qb = qb
                .select(db.raw('DATE(started_at) AS period'))
                .groupBy(db.raw('DATE(started_at)'))
                .orderBy('period', 'desc');
            break;

        case 'Week':
            qb = qb
                .select(
                    db.raw('EXTRACT(ISOYEAR FROM started_at) AS y'),
                    db.raw('EXTRACT(WEEK FROM started_at) AS w'),
                    db.raw(
                        "EXTRACT(ISOYEAR FROM started_at)||'-'||LPAD(EXTRACT(WEEK FROM started_at)::text,2,'0') AS period"
                    )
                )
                .groupBy('y', 'w')
                .orderBy([{ column: 'y', order: 'desc' }, { column: 'w', order: 'desc' }]);
            break;

        case 'Month':
            qb = qb
                .select(
                    db.raw('EXTRACT(YEAR FROM started_at) AS y'),
                    db.raw('EXTRACT(MONTH FROM started_at) AS m'),
                    db.raw(
                        "EXTRACT(YEAR FROM started_at)||'-'||LPAD(EXTRACT(MONTH FROM started_at)::text,2,'0') AS period"
                    )
                )
                .groupBy('y', 'm')
                .orderBy([{ column: 'y', order: 'desc' }, { column: 'm', order: 'desc' }]);
            break;
    }

    return qb;
}

module.exports = {
    listByCharger,
    findByIdForUser,
    insertSession,
    updateById,
    deleteByIdForUser,
    historyAgg,
};

// === Fim de: ./src/repositories/sessions.repo.js ===


// === Início de: ./src/repositories/users.repo.js ===
// src/repositories/users.repo.js
const db = require('../db/knex');

async function findByUsername(username) {
    return db('users').where({ username }).first();
}

async function insertUser({ username, passwordHash }) {
    const [row] = await db('users')
        .insert({ username, password: passwordHash })
        .returning('id');

    // pg pode devolver {id} ou o valor direto (depende config)
    return row?.id ?? row;
}

module.exports = { findByUsername, insertUser };

// === Fim de: ./src/repositories/users.repo.js ===


// === Início de: ./src/mqtt/index.js ===
// src/mqtt/index.js
const mqtt = require('mqtt');
const EventEmitter = require('events');

const db = require('../db/knex');
const config = require('../config');
const { sendPushToUser } = require('../services/push.service');

const { createPublishers } = require('./publishers');
const { createMqttContext } = require('./context');

const {
  handleStateEvse,
  handleStateScheduler,
  handleStateLoadbalancing,
  handleStateAuth,
  handleStateMetersConfig,
} = require('./handlers/evse.handler');
const { handleStateMeter } = require('./handlers/meter.handler');
const { handleLegacyState, handleLegacyConfigResponse } = require('./handlers/legacy.handler');

const emitter = new EventEmitter();

// --------------------
// MQTT client
// --------------------
const MQTT_URL = config.mqtt.url;
const mqttUser = config.mqtt.user;
const mqttPass = config.mqtt.pass;

const client = mqtt.connect(MQTT_URL, {
  username: mqttUser,
  password: mqttPass,
  reconnectPeriod: 2000,
});

// publishers separados
const publishers = createPublishers(client);

// ctx separado (helpers + caches + db helpers)
const ctx = createMqttContext({ db, config, emitter, sendPushToUser });

// --------------------
// Subscribe
// --------------------
client.on('connect', () => {
  console.log('[MQTT] Conectado ao broker:', MQTT_URL);

  const fixedTopics = [
    '+/state/#', // state/evse, state/scheduler, state/loadbalancing, state/meter/...
    '+/state', // legacy
    '+/response/#', // legacy
    '+/response/config/evse', // legacy
  ];

  const envTopics = config.mqtt.subTopics || [];
  const topicsToSub = [...new Set([...fixedTopics, ...envTopics])];

  topicsToSub.forEach((t) => {
    client.subscribe(t, { qos: 0 }, (err, granted) => {
      if (err) {
        console.error('[MQTT] Falha ao subscrever', t, err.message);
      } else {
        console.log('[MQTT] Subscrito:', granted?.map((g) => g.topic).join(', ') || t);
      }
    });
  });
});

// --------------------
// Messages
// --------------------
client.on('message', async (topic, message) => {
  const parts = topic.split('/');
  const mqttTopic = parts[0];
  const subtopic = parts.slice(1).join('/');

  const payload = ctx.safeJsonParse(message);
  if (!payload) {
    console.warn('[MQTT] JSON inválido em', topic, 'payload=', message.toString());
    return;
  }

  try {
    // -------- NOVOS TÓPICOS DO EVSE --------
    if (subtopic === 'state/evse') return await handleStateEvse(ctx, mqttTopic, payload);
    if (subtopic === 'state/scheduler')
      return await handleStateScheduler(ctx, mqttTopic, payload);
    if (subtopic === 'state/loadbalancing')
      return await handleStateLoadbalancing(ctx, mqttTopic, payload);
    if (subtopic === 'state/auth') return await handleStateAuth(ctx, mqttTopic, payload);
    if (subtopic === 'state/meters-config')
      return await handleStateMetersConfig(ctx, mqttTopic, payload);
    if (subtopic === 'state/meter/evse')
      return await handleStateMeter(ctx, mqttTopic, payload, 'evse');
    if (subtopic === 'state/meter/grid')
      return await handleStateMeter(ctx, mqttTopic, payload, 'grid');

    // -------- LEGACY COMPAT --------
    if (subtopic === 'state') return await handleLegacyState(ctx, mqttTopic, payload);
    if (subtopic === 'response/config/evse')
      return await handleLegacyConfigResponse(ctx, mqttTopic, payload);

    // fallback ignorado
  } catch (err) {
    console.error('[MQTT] Erro ao processar', topic, err);
  }
});

// --------------------
// Broker offline / checker
// --------------------
client.on('offline', async () => {
  console.warn('[MQTT] Broker offline');

  try {
    const chargers = await ctx.db('chargers').select('id', 'user_id', 'location');
    const uniqueUsers = [...new Set(chargers.map((c) => c.user_id))];

    await Promise.allSettled(
      uniqueUsers.map((userId) =>
        ctx.sendPushToUser(userId, {
          title: '📡 Broker MQTT offline',
          body: 'O sistema perdeu ligação ao broker. Alguns estados podem estar desatualizados.',
          url: '/',
        })
      )
    );
  } catch (err) {
    console.error('[MQTT] erro offline push:', err.message);
  }
});

setInterval(async () => {
  try {
    const timeoutMinutes = config.chargerOfflineMinutes;
    const limitDate = new Date(Date.now() - timeoutMinutes * 60 * 1000);

    const offlineChargers = await ctx.db('chargers')
      .where('updated_at', '<', limitDate.toISOString())
      .andWhereNot({ status: 'offline' })
      .select('*');

    for (const ch of offlineChargers) {
      await ctx.db('chargers').where({ id: ch.id }).update({ status: 'offline' });

      ctx.lastDbStateByChargerId.delete(ch.id);
      ctx.lastMetaByChargerId.delete(ch.id);
      ctx.lastTotalEnergyByChargerId.delete(ch.id);
      ctx.sessionStartEnergyByChargerId.delete(ch.id);

      ctx.lastEnabled[ch.id] = false;

      await ctx.sendPushToUser(ch.user_id, {
        title: '🔌 Carregador offline',
        body: `${ch.location || 'Carregador'} está offline há mais de ${timeoutMinutes} min.`,
        url: `/charger/${ch.id}`,
      });
    }
  } catch (err) {
    console.error('[MQTT] offline checker erro:', err.message);
  }
}, 60 * 1000);

// --------------------
// API pública (compat com o resto do projeto)
// --------------------
function on(event, handler) {
  emitter.on(event, handler);
}

module.exports = {
  on,
  ...publishers,
};

// === Fim de: ./src/mqtt/index.js ===


// === Início de: ./src/mqtt/publishers.js ===
// src/mqtt/publishers.js
function createPublishers(client) {
    /**
     * ✅ Firmware novo:
     * Para settings: <id>/cmd/evse/settings
     * payload: { currentLimit, temperatureLimit }
     */
    function sendEvseSettings(chargerTopic, settings = {}) {
      const payload = {};
      if (settings.currentLimit !== undefined) payload.currentLimit = Number(settings.currentLimit);
      if (settings.temperatureLimit !== undefined)
        payload.temperatureLimit = Number(settings.temperatureLimit);
  
      if (!Object.keys(payload).length) return;
  
      client.publish(`${chargerTopic}/cmd/evse/settings`, JSON.stringify(payload), { qos: 1 });
    }
  
    // compat + mapeamento
    function sendConfig(chargerTopic, property, value) {
      const map = {
        maxChargingCurrent: 'currentLimit',
        temperatureThreshold: 'temperatureLimit',
      };
  
      if (map[property]) {
        return sendEvseSettings(chargerTopic, { [map[property]]: value });
      }
  
      // fallback legacy
      const payload = { [property]: value };
      client.publish(`${chargerTopic}/set/config/evse`, JSON.stringify(payload), { qos: 1 });
    }
  
    // legacy (mantidos)
    function sendEnable(chargerTopic, enable) {
      client.publish(`${chargerTopic}/enable`, JSON.stringify({ enable: !!enable }), { qos: 1 });
    }
  
    function requestConfig(chargerTopic) {
      client.publish(`${chargerTopic}/request/config/evse`, null, { qos: 1 });
    }
  
    // helper genérico novo
    function sendCmd(chargerTopic, cmdSubtopic, obj) {
      const t = `${chargerTopic}/cmd/${cmdSubtopic}`;
      const msg = obj ? JSON.stringify(obj) : '';
      client.publish(t, msg, { qos: 1 });
    }
  
    return { sendConfig, sendEvseSettings, sendEnable, requestConfig, sendCmd };
  }
  
  module.exports = { createPublishers };
  
// === Fim de: ./src/mqtt/publishers.js ===


// === Início de: ./src/mqtt/context.js ===
// src/mqtt/context.js
function createMqttContext({ db, config, emitter, sendPushToUser }) {
    // --------------------
    // Helpers
    // --------------------
    const toNum = (v) => {
        if (v === null || v === undefined || v === '') return 0;
        const n = typeof v === 'number' ? v : parseFloat(v);
        return Number.isFinite(n) ? n : 0;
    };

    const round1 = (v) => Math.round(toNum(v) * 10) / 10;
    const round2 = (v) => Math.round(toNum(v) * 100) / 100;

    const wToKw2 = (w) => round2(toNum(w) / 1000);

    const toArr3 = (v) => {
        if (Array.isArray(v)) return [round1(v[0]), round1(v[1]), round1(v[2])];
        if (v && typeof v === 'object') return [round1(v.l1), round1(v.l2), round1(v.l3)];
        return [0, 0, 0];
    };

    const toArr3Kw2 = (v) => {
        if (Array.isArray(v)) return [wToKw2(v[0]), wToKw2(v[1]), wToKw2(v[2])];
        if (v && typeof v === 'object') return [wToKw2(v.l1), wToKw2(v.l2), wToKw2(v.l3)];
        return [0, 0, 0];
    };

    function safeJsonParse(buf) {
        try {
            return JSON.parse(buf.toString());
        } catch {
            return null;
        }
    }

    function shallowEqual(a, b) {
        if (a === b) return true;
        if (!a || !b) return false;

        const ak = Object.keys(a);
        const bk = Object.keys(b);
        if (ak.length !== bk.length) return false;

        for (const k of ak) {
            const av = a[k];
            const bv = b[k];

            if (Array.isArray(av) || Array.isArray(bv)) {
                if (!Array.isArray(av) || !Array.isArray(bv)) return false;
                if (av.length !== bv.length) return false;
                for (let i = 0; i < av.length; i++) {
                    if (av[i] !== bv[i]) return false;
                }
            } else if (av !== bv) {
                return false;
            }
        }

        return true;
    }

    function inferStateCode(raw) {
        const s = String(raw || '').trim();
        if (!s) return '';
        return s.split(/\s+/)[0] || '';
    }

    function getStatusFromStateCode(code) {
        const map = {
            A: '🔌 Not Conn.',
            A1: '🔌 Not Conn.',
            B1: '🟡 Unauth.',
            B2: '🟢 Ready',
            C1: '⚡ Wait',
            C2: '⚡ Charging',
            D1: '💨 Vent (req)',
            D2: '💨 Vent',
            E: '❌ CP Error',
            F: '⚠️ Fault',
        };
        return map[code] || '❓ Unknown';
    }

    function stripUndef(obj) {
        return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined));
    }

    // --------------------
    // Caches
    // --------------------
    const lastDbStateByChargerId = new Map(); // chargerId -> { ...dbFields }
    const lastMetaByChargerId = new Map(); // chargerId -> { status, stateCode, rawStatus }
    const lastTotalEnergyByChargerId = new Map(); // chargerId -> totalEnergy (kWh)
    const sessionStartEnergyByChargerId = new Map(); // chargerId -> totalEnergy at session start
    const lastEnabled = {}; // chargerId -> boolean

    const chargerCache = new Map(); // mqttTopic -> { charger, fetchedAt }
    const CHARGER_CACHE_TTL_MS = config.chargerCacheTtlMs;

    // --------------------
    // DB helpers
    // --------------------
    async function getChargerByMqttTopic(mqttTopic) {
        const cached = chargerCache.get(mqttTopic);
        const now = Date.now();

        if (cached && now - cached.fetchedAt < CHARGER_CACHE_TTL_MS) {
            return cached.charger;
        }

        const charger = await db('chargers').where({ mqtt_topic: mqttTopic }).first();
        if (charger) chargerCache.set(mqttTopic, { charger, fetchedAt: now });
        return charger;
    }

    async function updateChargerDbIfChanged(chargerId, partialUpdate) {
        const prev = lastDbStateByChargerId.get(chargerId) || null;

        const cleanPartial = stripUndef(partialUpdate);
        const next = { ...(prev || {}), ...cleanPartial };

        const changed = !prev || !shallowEqual(prev, next);
        if (!changed) return false;

        await db('chargers').where({ id: chargerId }).update(cleanPartial);
        lastDbStateByChargerId.set(chargerId, next);
        return true;
    }

    // ctx é o “contrato” que os handlers usam
    return {
        db,
        config,
        emitter,
        sendPushToUser,

        // helpers
        toNum,
        round1,
        round2,
        wToKw2,
        toArr3,
        toArr3Kw2,
        safeJsonParse,
        shallowEqual,
        inferStateCode,
        getStatusFromStateCode,
        stripUndef,

        // caches
        lastDbStateByChargerId,
        lastMetaByChargerId,
        lastTotalEnergyByChargerId,
        sessionStartEnergyByChargerId,
        lastEnabled,

        // db helpers
        getChargerByMqttTopic,
        updateChargerDbIfChanged,
    };
}

module.exports = { createMqttContext };

// === Fim de: ./src/mqtt/context.js ===


// === Início de: ./src/mqtt/handlers/legacy.handler.js ===
// src/mqtt/handlers/legacy.handler.js
async function handleLegacyState(ctx, mqttTopic, payload) {
    const charger = await ctx.getChargerByMqttTopic(mqttTopic);
    if (!charger) {
        console.warn(`[MQTT] Charger não encontrado para topic: ${mqttTopic}`);
        return;
    }

    const chargerId = charger.id;
    const now = new Date();

    const stateCode = ctx.inferStateCode(payload?.state);
    if (!stateCode) return;

    const status = ctx.getStatusFromStateCode(stateCode);
    ctx.lastMetaByChargerId.set(chargerId, { status, stateCode, rawStatus: payload?.state });

    // legacy arrays (assumimos W -> converter p/ kW)
    const powerArrKw = ctx.toArr3Kw2(payload?.power);
    const voltageArr = ctx.toArr3(payload?.voltage);
    const currentArr = ctx.toArr3(payload?.current);

    const consumption = ctx.round2(payload?.consumption);
    const chargingTime = ctx.round1(payload?.chargingTime ?? payload?.sessionTime);

    const dbUpdate = {
        status,
        charging_current: currentArr[0],
        consumption,
        charging_time: chargingTime,

        power_l1: powerArrKw[0],
        power_l2: powerArrKw[1],
        power_l3: powerArrKw[2],

        voltage_l1: voltageArr[0],
        voltage_l2: voltageArr[1],
        voltage_l3: voltageArr[2],

        current_l1: currentArr[0],
        current_l2: currentArr[1],
        current_l3: currentArr[2],

        updated_at: now.toISOString(),
    };

    await ctx.updateChargerDbIfChanged(chargerId, dbUpdate);

    ctx.emitter.emit('charging-status', {
        charger_id: chargerId,
        mqtt_topic: mqttTopic,
        status,
        stateCode,
        consumption,
        chargingTime,
        power: powerArrKw,
        voltage: voltageArr,
        current: currentArr,
        raw: payload,
    });
}

async function handleLegacyConfigResponse(ctx, mqttTopic, payload) {
    const charger = await ctx.getChargerByMqttTopic(mqttTopic);
    if (!charger) return;

    const configData = {
        charger_id: charger.id,
        max_charging_current: payload?.maxChargingCurrent || 32,
        require_auth: !!payload?.requireAuth,
        rcm_enabled: !!payload?.rcm,
        temperature_limit: payload?.temperatureThreshold || 60,
        config_received_at: new Date().toISOString(),
    };

    const existingConfig = await ctx.db('charger_configs').where({ charger_id: charger.id }).first();

    if (existingConfig) {
        await ctx.db('charger_configs').where({ charger_id: charger.id }).update(configData);
    } else {
        await ctx.db('charger_configs').insert(configData);
    }

    ctx.emitter.emit('charging-config', {
        ...configData,
        mqtt_topic: mqttTopic,
        raw: payload,
    });
}

module.exports = { handleLegacyState, handleLegacyConfigResponse };

// === Fim de: ./src/mqtt/handlers/legacy.handler.js ===


// === Início de: ./src/mqtt/handlers/evse.handler.js ===
// src/mqtt/handlers/evse.handler.js
async function handleStateEvse(ctx, mqttTopic, payload) {
    const charger = await ctx.getChargerByMqttTopic(mqttTopic);
    if (!charger) {
        console.warn(`[MQTT] Charger não encontrado para topic: ${mqttTopic}`);
        return;
    }

    const chargerId = charger.id;
    const now = new Date();

    const rawStatus = payload?.status || payload?.state || '';
    const stateCode = ctx.inferStateCode(rawStatus);
    if (!stateCode) {
        console.warn(`[MQTT] state/evse sem stateCode válido (charger ${chargerId})`);
        return;
    }

    const status = ctx.getStatusFromStateCode(stateCode);
    ctx.lastMetaByChargerId.set(chargerId, { status, stateCode, rawStatus });

    const isCharging = stateCode === 'C2';

    // do firmware: chargers[0].current / power (W)
    const ch0 = Array.isArray(payload?.chargers) ? payload.chargers[0] : null;

    // ⚠️ current no state/evse costuma ser limite (mesmo sem estar a carregar)
    const currentA = isCharging ? ctx.round1(ch0?.current) : 0;

    // ⚠️ power no state/evse vem em W -> guardar kW (e só quando está a carregar)
    const powerKw = isCharging ? ctx.wToKw2(ch0?.power) : 0;

    const dbUpdate = {
        status,
        charging_current: currentA,
        power_l1: powerKw,
        power_l2: 0,
        power_l3: 0,
        current_l1: currentA,
        current_l2: 0,
        current_l3: 0,
        updated_at: now.toISOString(),
    };

    await ctx.updateChargerDbIfChanged(chargerId, dbUpdate);

    // Sessões start/stop baseado em C2
    const previouslyEnabled = ctx.lastEnabled[chargerId] || false;
    const currentlyEnabled = isCharging;

    if (!previouslyEnabled && currentlyEnabled) {
        const startEnergy = ctx.lastTotalEnergyByChargerId.get(chargerId) ?? 0;
        ctx.sessionStartEnergyByChargerId.set(chargerId, startEnergy);

        const activeSession = await ctx.db('charger_sessions')
            .where({ charger_id: chargerId })
            .whereNull('ended_at')
            .first();

        if (!activeSession) {
            await ctx.db('charger_sessions').insert({
                charger_id: chargerId,
                started_at: now,
                kwh: 0,
            });
            console.log(`[DB] Sessão iniciada para charger ID ${chargerId}`);
        }
    }

    if (previouslyEnabled && !currentlyEnabled) {
        const session = await ctx.db('charger_sessions')
            .where({ charger_id: chargerId })
            .whereNull('ended_at')
            .first();

        if (session) {
            const endEnergy = ctx.lastTotalEnergyByChargerId.get(chargerId) ?? 0;
            const startEnergy = ctx.sessionStartEnergyByChargerId.get(chargerId) ?? 0;
            const delta = Math.max(0, ctx.round2(endEnergy - startEnergy));

            await ctx.db('charger_sessions')
                .where({ id: session.id })
                .update({ ended_at: now, kwh: delta });

            console.log(`[DB] Sessão finalizada para charger ID ${chargerId} (kWh=${delta})`);
        }

        ctx.sessionStartEnergyByChargerId.delete(chargerId);

        await ctx.sendPushToUser(charger.user_id, {
            title: '✅ Carregamento concluído',
            body: `${charger.location || 'Carregador'} terminou o carregamento.`,
            url: `/history`,
        });
    }

    ctx.lastEnabled[chargerId] = currentlyEnabled;

    ctx.emitter.emit('charging-status', {
        charger_id: chargerId,
        mqtt_topic: mqttTopic,
        status,
        stateCode,
        consumption: ctx.lastTotalEnergyByChargerId.get(chargerId) ?? 0,
        chargingTime: ctx.toNum(payload?.chargingTime) || 0,
        power: [powerKw, 0, 0],
        voltage: [0, 0, 0],
        current: [currentA, 0, 0],
        raw: payload,
    });

    if (status === '⚠️ Fault' || status === '❌ CP Error') {
        await ctx.sendPushToUser(charger.user_id, {
            title: '⚠️ Erro no carregador',
            body: `${charger.location || 'Carregador'} entrou em falha.`,
            url: `/charger/${charger.id}`,
        });
    }
}

async function handleStateScheduler(ctx, mqttTopic, payload) {
    const charger = await ctx.getChargerByMqttTopic(mqttTopic);
    if (!charger) return;

    ctx.emitter.emit('scheduler-state', {
        charger_id: charger.id,
        mqtt_topic: mqttTopic,
        ...payload,
        updated_at: new Date().toISOString(),
    });
}

async function handleStateLoadbalancing(ctx, mqttTopic, payload) {
    const charger = await ctx.getChargerByMqttTopic(mqttTopic);
    if (!charger) return;

    ctx.emitter.emit('loadbalancing-state', {
        charger_id: charger.id,
        mqtt_topic: mqttTopic,
        ...payload,
        updated_at: new Date().toISOString(),
    });
}

async function handleStateAuth(ctx, mqttTopic, payload) {
    const charger = await ctx.getChargerByMqttTopic(mqttTopic);
    if (!charger) return;

    ctx.emitter.emit('auth-state', {
        charger_id: charger.id,
        mqtt_topic: mqttTopic,
        ...payload,
        updated_at: new Date().toISOString(),
    });
}

async function handleStateMetersConfig(ctx, mqttTopic, payload) {
    const charger = await ctx.getChargerByMqttTopic(mqttTopic);
    if (!charger) return;

    ctx.emitter.emit('meters-config', {
        charger_id: charger.id,
        mqtt_topic: mqttTopic,
        ...payload,
        updated_at: new Date().toISOString(),
    });
}

module.exports = {
    handleStateEvse,
    handleStateScheduler,
    handleStateLoadbalancing,
    handleStateAuth,
    handleStateMetersConfig,
};

// === Fim de: ./src/mqtt/handlers/evse.handler.js ===


// === Início de: ./src/mqtt/handlers/meter.handler.js ===
// src/mqtt/handlers/meter.handler.js
async function handleStateMeter(ctx, mqttTopic, payload, meterKind /* 'evse'|'grid' */) {
    const charger = await ctx.getChargerByMqttTopic(mqttTopic);
    if (!charger) {
        console.warn(`[MQTT] Charger não encontrado para topic: ${mqttTopic}`);
        return;
    }

    const chargerId = charger.id;
    const now = new Date();

    const vrms = ctx.toArr3(payload?.vrms);
    const irms = ctx.toArr3(payload?.irms);

    // ✅ watt vem em W -> guardar kW no DB
    const wattKw = ctx.toArr3Kw2(payload?.watt);

    const totalEnergy = ctx.round2(payload?.totalEnergy); // acumulado (kWh)
    const source = String(payload?.source || meterKind || '').toUpperCase();

    if (Number.isFinite(totalEnergy) && totalEnergy >= 0) {
        ctx.lastTotalEnergyByChargerId.set(chargerId, totalEnergy);
    }

    // só gravamos métricas no DB para EVSE (normalmente o que interessa)
    if (meterKind === 'evse' || source === 'EVSE') {
        const dbUpdate = {
            consumption: totalEnergy,
            charging_current: irms[0],

            power_l1: wattKw[0],
            power_l2: wattKw[1],
            power_l3: wattKw[2],

            voltage_l1: vrms[0],
            voltage_l2: vrms[1],
            voltage_l3: vrms[2],

            current_l1: irms[0],
            current_l2: irms[1],
            current_l3: irms[2],

            updated_at: now.toISOString(),
        };

        await ctx.updateChargerDbIfChanged(chargerId, dbUpdate);

        // atualiza sessão ativa com delta (sem fechar)
        const currentlyEnabled = ctx.lastEnabled[chargerId] || false;
        if (currentlyEnabled) {
            const session = await ctx.db('charger_sessions')
                .where({ charger_id: chargerId })
                .whereNull('ended_at')
                .first();

            if (session) {
                const startEnergy = ctx.sessionStartEnergyByChargerId.get(chargerId) ?? 0;
                const delta = Math.max(
                    0,
                    ctx.round2((ctx.lastTotalEnergyByChargerId.get(chargerId) ?? 0) - startEnergy)
                );

                await ctx.db('charger_sessions').where({ id: session.id }).update({ kwh: delta });
            }
        }

        const meta = ctx.lastMetaByChargerId.get(chargerId) || { status: '—', stateCode: undefined };
        ctx.emitter.emit('charging-status', {
            charger_id: chargerId,
            mqtt_topic: mqttTopic,
            status: meta.status,
            stateCode: meta.stateCode,
            consumption: totalEnergy,
            chargingTime: 0,
            power: wattKw,
            voltage: vrms,
            current: irms,
            raw: payload,
        });
    }

    ctx.emitter.emit('meter-live', {
        charger_id: chargerId,
        mqtt_topic: mqttTopic,
        meter: meterKind,
        vrms,
        irms,
        watt: wattKw,
        totalEnergy,
        raw: payload,
        updated_at: now.toISOString(),
    });
}

module.exports = { handleStateMeter };

// === Fim de: ./src/mqtt/handlers/meter.handler.js ===


// === Início de: ./src/config/index.js ===
// src/config/index.js
require('dotenv').config();

function must(name) {
  const v = process.env[name];
  if (!v) throw new Error(`${name} não definido no .env`);
  return v;
}

const config = {
  env: process.env.NODE_ENV || 'development',

  port: Number(process.env.PORT || 4000),

  jwtSecret: must('JWT_SECRET'),

  corsOrigins: (process.env.CORS_ORIGIN
    ? process.env.CORS_ORIGIN.split(',').map((s) => s.trim())
    : ['http://localhost:5173']
  ).filter(Boolean),

  mqtt: {
    url: process.env.MQTT_URL || 'mqtt://localhost:1883',
    user: process.env.MQTT_USER || 'admin',
    pass: process.env.MQTT_PASS || '123QWEasd',
    subTopics: (process.env.MQTT_SUB_TOPICS || '')
      .split(',')
      .map((s) => s.trim())
      .filter(Boolean),
  },

  chargerOfflineMinutes: Number(process.env.CHARGER_OFFLINE_MINUTES || 5),
  chargerCacheTtlMs: Number(process.env.CHARGER_CACHE_TTL_MS || 30000),

  vapid: {
    publicKey: process.env.VAPID_PUBLIC_KEY || '',
    privateKey: process.env.VAPID_PRIVATE_KEY || '',
    subject: process.env.VAPID_SUBJECT || 'mailto:admin@evstation.local',
  },
};

module.exports = config;

// === Fim de: ./src/config/index.js ===


// === Início de: ./knexfile.js ===
// knexfile.js
require('dotenv').config();

function must(name) {
  const v = process.env[name];
  if (!v) throw new Error(`${name} não definido no .env`);
  return v;
}

const shared = {
  client: 'pg',
  migrations: {
    directory: './src/db/migrations',
  },
};

function buildConnectionFromEnv() {
  // Se houver DATABASE_URL, usa-o.
  // Se PGSSL=true, aplica ssl no formato esperado pelo driver pg (dentro de connection).
  const ssl =
    process.env.PGSSL === 'true' ? { rejectUnauthorized: false } : undefined;

  if (process.env.DATABASE_URL) {
    // knex aceita string, mas o ssl precisa estar no objeto:
    return ssl
      ? { connectionString: process.env.DATABASE_URL, ssl }
      : process.env.DATABASE_URL;
  }

  // fallback para vars soltas
  return {
    host: process.env.PGHOST || '127.0.0.1',
    port: Number(process.env.PGPORT || 5432),
    user: process.env.PGUSER || 'postgres',
    password: process.env.PGPASSWORD || 'postgres',
    database: process.env.PGDATABASE || 'evse',
    ...(ssl ? { ssl } : {}),
  };
}

module.exports = {
  development: {
    ...shared,
    connection: buildConnectionFromEnv(),
  },

  production: {
    ...shared,
    // Em produção normalmente queres obrigar DATABASE_URL (se for o teu caso):
    // connection: must('DATABASE_URL'),
    // Mas mantendo compatível com vars soltas:
    connection: buildConnectionFromEnv(),
    pool: { min: 2, max: 10 },
  },
};

// === Fim de: ./knexfile.js ===


// === Início de: ./package.json ===
{
  "name": "evse-backend",
  "version": "1.0.0",
  "main": "src/server.js",
  "scripts": {
    "start": "node src/server.js",
    "dev": "nodemon src/server.js",
    "migrate": "knex migrate:latest",
    "rollback": "knex migrate:rollback"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "axios": "^1.13.2",
    "bcryptjs": "^3.0.2",
    "dotenv": "^16.5.0",
    "express": "^5.1.0",
    "express-rate-limit": "^8.2.1",
    "express-validator": "^7.2.1",
    "jsonwebtoken": "^9.0.2",
    "knex": "^3.1.0",
    "mqtt": "^5.13.1",
    "pg": "^8.16.0",
    "socket.io": "^4.8.1",
    "web-push": "^3.6.7"
  },
  "devDependencies": {
    "nodemon": "^3.1.10"
  }
}

// === Fim de: ./package.json ===
