import mongoose from 'mongoose';
import path from 'path';
import { fileURLToPath } from 'url';

import Cache from '../libs/cache.js';

const { Schema } = mongoose;

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const ClientsCacheFile = path.join(__dirname, '../data/clients.cache');

const clientSettingSchema = new Schema({
  _id: {
    required: true,
    type: String,
  },
  deviceId: {
    required: true,
    type: Number,
  },
}, { timestamps: true });

const ClientSetting = mongoose.model('ClientSetting', clientSettingSchema);

const CLIENT_HEADER_FIELDS = [
  'X-Device',
  'X-Version',
  'X-Data-Type',
  'X-Market-Type',
  'X-Group-Sequence',
  'X-Platform',
];

const CLIENT_KEY_FIELDS = [
  'X-Data-Type',
  'X-Market-Type',
  'X-Group-Sequence',
  'X-Platform',
];

const CLIENT_FIELD_NAMES = {
  'X-Data-Type': 'dataType',
  'X-Device': 'device',
  'X-Group-Sequence': 'groupSequence',
  'X-Market-Type': 'marketType',
  'X-Platform': 'platform',
  'X-Version': 'version',
};

const CLIENTS = {
  Items: {},
  LegacySettings: {},
  Settings: {},
};

let saveTimer = null;

const normalizeHeaderValue = (value) => {
  if (Array.isArray(value)) {
    return value.join(',');
  }
  return value == null ? '' : String(value).trim();
}

const getRequestHeader = (req, field) => {
  return normalizeHeaderValue(req.get?.(field));
}

const getClientHeaders = (req) => {
  return CLIENT_HEADER_FIELDS.reduce((headers, field) => {
    headers[field] = getRequestHeader(req, field);
    return headers;
  }, {});
}

const formatClientFields = (headers) => {
  return CLIENT_HEADER_FIELDS.reduce((clientFields, field) => {
    clientFields[CLIENT_FIELD_NAMES[field]] = headers[field];
    return clientFields;
  }, {});
}

const getClientKey = (headers) => {
  return CLIENT_KEY_FIELDS
  .map(field => `${field}:${headers[field] ?? ''}`)
  .join('|');
}

const getClientIp = (req) => {
  const ip = getRequestHeader(req, 'X-Real-IP') ||
    normalizeHeaderValue(req.ip) ||
    normalizeHeaderValue(req.socket?.remoteAddress) ||
    normalizeHeaderValue(req.connection?.remoteAddress);
  return ip.replace(/^::ffff:/, '');
}

const scheduleSaveClientsToCache = () => {
  if (saveTimer) {
    clearTimeout(saveTimer);
  }
  saveTimer = setTimeout(saveClientsToCache, 1000);
}

const recordRequest = (req) => {
  const route = req.path;
  const headers = getClientHeaders(req);
  const clientFields = formatClientFields(headers);
  const key = getClientKey(headers);
  const now = Date.now();
  const current = CLIENTS.Items[key] ?? {};
  const setting = CLIENTS.Settings[key] ?? {};

  CLIENTS.Items[key] = {
    key,
    ...clientFields,
    deviceId: setting.deviceId ?? current.deviceId,
    ip: getClientIp(req),
    route,
    firstRequestTime: current.firstRequestTime ?? now,
    lastRequestTime: now,
    requestCount: (current.requestCount ?? 0) + 1,
  };

  scheduleSaveClientsToCache();
  return CLIENTS.Items[key];
}

const getClients = () => {
  return Object.values(CLIENTS.Items)
  .map(client => ({
    ...client,
    deviceId: CLIENTS.Settings[client.key]?.deviceId ?? client.deviceId,
  }))
  .sort((a, b) => (b.lastRequestTime ?? 0) - (a.lastRequestTime ?? 0));
}

const updateClient = async ({ key, deviceId } = {}) => {
  if (!key || !CLIENTS.Items[key]) {
    return Promise.reject(new Error('CLIENT_NOT_FOUND'));
  }
  if (deviceId === undefined || deviceId === null || deviceId === '') {
    delete CLIENTS.Settings[key];
    delete CLIENTS.Items[key].deviceId;
    await ClientSetting.deleteOne({ _id: key });
    saveClientsToCache();
    return CLIENTS.Items[key];
  }
  const parsedDeviceId = Number(deviceId);
  if (!Number.isInteger(parsedDeviceId)) {
    return Promise.reject(new Error('DEVICE_ID_INVALID'));
  }
  await ClientSetting.findByIdAndUpdate(
    key,
    { $set: { deviceId: parsedDeviceId } },
    { new: true, upsert: true },
  );
  CLIENTS.Settings[key] = { deviceId: parsedDeviceId };
  CLIENTS.Items[key] = {
    ...CLIENTS.Items[key],
    deviceId: parsedDeviceId,
  };
  saveClientsToCache();
  return CLIENTS.Items[key];
}

const deleteClient = async (key) => {
  if (!key || !CLIENTS.Items[key]) {
    return Promise.reject(new Error('CLIENT_NOT_FOUND'));
  }
  delete CLIENTS.Items[key];
  delete CLIENTS.Settings[key];
  await ClientSetting.deleteOne({ _id: key });
  saveClientsToCache();
}

const loadClientSettings = async () => {
  const legacyEntries = Object.entries(CLIENTS.LegacySettings);
  if (legacyEntries.length) {
    await Promise.all(legacyEntries.map(([key, setting]) => {
      return ClientSetting.updateOne(
        { _id: key },
        { $setOnInsert: { deviceId: setting.deviceId } },
        { upsert: true },
      );
    }));
    CLIENTS.LegacySettings = {};
  }

  const settings = await ClientSetting.find().lean();
  CLIENTS.Settings = settings.reduce((map, setting) => {
    map[setting._id] = { deviceId: setting.deviceId };
    if (CLIENTS.Items[setting._id]) {
      CLIENTS.Items[setting._id].deviceId = setting.deviceId;
    }
    return map;
  }, {});
  return CLIENTS.Settings;
}

function saveClientsToCache() {
  if (saveTimer) {
    clearTimeout(saveTimer);
    saveTimer = null;
  }
  Cache.setData(ClientsCacheFile, { Items: CLIENTS.Items });
}

function loadClientsFromCache() {
  const cachedClients = Cache.getData(ClientsCacheFile, true);
  if (!cachedClients?.Items) {
    return;
  }
  CLIENTS.Items = cachedClients.Items;
  Object.values(CLIENTS.Items).forEach(client => {
    if (Number.isInteger(client.deviceId)) {
      CLIENTS.LegacySettings[client.key] = { deviceId: client.deviceId };
    }
    delete client.deviceId;
  });
}

loadClientsFromCache();
loadClientSettings().catch(() => {});

process.on('exit', saveClientsToCache);
process.on('SIGINT', () => {
  process.exit(0);
});
process.on('SIGTERM', () => {
  process.exit(0);
});
process.on('SIGUSR2', () => {
  process.exit(0);
});

const Clients = {
  recordRequest,
  getClients,
  updateClient,
  deleteClient,
  loadClientSettings,
};

export { recordRequest, getClients, updateClient, deleteClient, loadClientSettings };
export default Clients;
