const { Queue } = require('bullmq');
const IORedis = require('ioredis');
const config = require('./config');
const logger = require('./logger');
const { hashObject, nowIso } = require('./utils');

const redis = new IORedis(config.redisUrl, {
  maxRetriesPerRequest: null
});

const signalsQueue = new Queue('signals', {
  connection: redis,
  defaultJobOptions: {
    attempts: 5,
    backoff: {
      type: 'exponential',
      delay: 5000
    },
    removeOnComplete: 200,
    removeOnFail: 500
  }
});

function parseOdds(value) {
  if (value == null) return 1;
  const normalized = String(value).replace(',', '.').match(/-?\d+(\.\d+)?/);
  const parsed = normalized ? Number(normalized[0]) : NaN;
  return Number.isFinite(parsed) && parsed > 0 ? parsed : 1;
}

function parseLineValue(value) {
  if (value == null) return null;
  const normalized = String(value).replace(',', '.').match(/-?\d+(\.\d+)?/);
  const parsed = normalized ? Number(normalized[0]) : NaN;
  return Number.isFinite(parsed) ? parsed : null;
}

function parseEventStartTime(item) {
  const now = new Date();
  const candidates = [item.date, item.eventTime];
  for (const candidate of candidates) {
    if (!candidate) continue;

    const isoDate = new Date(candidate);
    if (!Number.isNaN(isoDate.getTime())) {
      return isoDate;
    }

    const partial = String(candidate).match(/(\d{1,2})[./](\d{1,2})(?:[./](\d{2,4}))?.*?(\d{1,2}):(\d{2})/);
    if (partial) {
      const year = partial[3] ? Number(partial[3].length === 2 ? `20${partial[3]}` : partial[3]) : new Date().getFullYear();
      const parsed = new Date(year, Number(partial[2]) - 1, Number(partial[1]), Number(partial[4]), Number(partial[5]));
      if (!Number.isNaN(parsed.getTime())) {
        return parsed;
      }
    }

    const timeOnly = String(candidate).match(/(?:^|\s)(\d{1,2}):(\d{2})(?:\s|$)/);
    if (timeOnly) {
      const parsed = new Date(
        now.getFullYear(),
        now.getMonth(),
        now.getDate(),
        Number(timeOnly[1]),
        Number(timeOnly[2]),
        0,
        0
      );
      if (!Number.isNaN(parsed.getTime())) {
        return parsed;
      }
    }
  }

  return new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0);
}

function parseRelativeSignalAge(value, reference = new Date()) {
  if (!value) return null;

  const normalized = String(value).trim().toLowerCase();
  if (!normalized) return null;
  if (/^(just now|now|С‚РѕР»СЊРєРѕ С‡С‚Рѕ|СЃРµР№С‡Р°СЃ)$/i.test(normalized)) {
    return new Date(reference.getTime());
  }

  const match = normalized.match(
    /(\d+)\s*(СЃРµРє(?:СѓРЅРґР°|СѓРЅРґС‹|СѓРЅРґ)?|sec(?:ond)?s?|СЃ|min(?:ute)?s?|РјРёРЅ(?:СѓС‚Р°|СѓС‚С‹|СѓС‚)?|m|С‡Р°СЃ(?:Р°|РѕРІ)?|С‡|hour(?:s)?|hr|h|Рґ(?:РµРЅСЊ|РЅСЏ|РЅРµР№)?|РґРЅ(?:СЏ|РµР№)?|day(?:s)?|РЅРµРґ(?:РµР»СЏ|РµР»Рё|РµР»СЊ)?|week(?:s)?|w)/i
  );

  if (!match) {
    return null;
  }

  const amount = Number(match[1]);
  if (!Number.isFinite(amount) || amount < 0) {
    return null;
  }

  const unit = match[2].toLowerCase();
  let diffMs = 0;

  if (/^(СЃРµРє|sec|СЃ)/i.test(unit)) {
    diffMs = amount * 1000;
  } else if (/^(min|РјРёРЅ|m)/i.test(unit)) {
    diffMs = amount * 60 * 1000;
  } else if (/^(С‡Р°СЃ|С‡|hour|hr|h)/i.test(unit)) {
    diffMs = amount * 60 * 60 * 1000;
  } else if (/^(Рґ|РґРЅ|day)/i.test(unit)) {
    diffMs = amount * 24 * 60 * 60 * 1000;
  } else if (/^(РЅРµРґ|week|w)/i.test(unit)) {
    diffMs = amount * 7 * 24 * 60 * 60 * 1000;
  } else {
    return null;
  }

  return new Date(reference.getTime() - diffMs);
}

function parseSignalTime(item) {
  const publicationTimer = Number(item.publicationTimer);
  if (Number.isFinite(publicationTimer) && publicationTimer >= 0) {
    return new Date(Date.now() - publicationTimer * 1000);
  }

  const explicitCandidates = [
    item.signalTime,
    item.publishedAt,
    item.publicationTime
  ];

  for (const candidate of explicitCandidates) {
    if (!candidate) continue;

    const parsed = candidate instanceof Date ? candidate : new Date(candidate);
    if (!Number.isNaN(parsed.getTime())) {
      return parsed;
    }
  }

  const relativeCandidates = [
    item.publicationAge
  ];

  for (const candidate of relativeCandidates) {
    const parsed = parseRelativeSignalAge(candidate);
    if (parsed && !Number.isNaN(parsed.getTime())) {
      return parsed;
    }
  }

  return new Date();
}

function createEventId(item) {
  const botPrefix = item.botKey ? `${item.botKey}:` : '';
  return (
    (item.id && `${botPrefix}${item.id}`) ||
    (item.eventUrl && `${botPrefix}${item.eventUrl}`) ||
    `${botPrefix}${hashObject({
      homeTeam: item.homeTeam,
      awayTeam: item.awayTeam,
      tournament: item.tournament,
      title: item.title
    }).slice(0, 24)}`
  );
}

function createSelection(item) {
  return item.selectionText || item.title || item.stake || 'unknown_selection';
}

function createMarketType(item) {
  return item.publicationType || 'parser_market';
}

function createDedupeKey(mapped) {
  return [
    mapped.providerId,
    mapped.rawPayload?.botKey || 'default',
    mapped.eventId.trim().toLowerCase(),
    mapped.marketType.trim().toLowerCase(),
    mapped.selection.trim().toLowerCase(),
    mapped.lineValue ?? 'na'
  ].join(':');
}

function parseActiveTab(value) {
  if (value === null || value === undefined || value === '') return null;
  const parsed = Number(value);
  return Number.isInteger(parsed) ? parsed : null;
}

function getSignalPriority(signal) {
  const activeTab = parseActiveTab(signal?.rawPayload?.activeTab);
  if (activeTab === 1) return 2;
  if (activeTab === 2) return 1;
  return 0;
}

function pickBestCandidate(existing, candidate) {
  if (!existing) return candidate;
  const existingPriority = getSignalPriority(existing.signal);
  const candidatePriority = getSignalPriority(candidate.signal);

  if (candidatePriority > existingPriority) {
    return candidate;
  }

  return existing;
}

function mapParserItemToSignal(item) {
  const mapped = {
    providerId: config.providerId,
    eventId: createEventId(item),
    sportType: item.sport || 'unknown',
    leagueName: item.tournament || 'Unknown league',
    homeTeam: item.homeTeam || item.title || 'Unknown home',
    awayTeam: item.awayTeam || 'Unknown away',
    eventStartTime: parseEventStartTime(item),
    marketType: createMarketType(item),
    selection: createSelection(item),
    forecast: item.forecast || null,
    lineValue: parseLineValue(item.stakePercent),
    odds: parseOdds(item.coef),
    signalTime: parseSignalTime(item),
    status: 'pending',
    sourceType: 'provider',
    comment: item.rawText || null,
    published: true,
    rawPayload: item
  };

  return {
    ...mapped,
    dedupeKey: createDedupeKey(mapped)
  };
}

function serializeSignal(signal) {
  return {
    ...signal,
    eventStartTime: signal.eventStartTime.toISOString(),
    signalTime: signal.signalTime.toISOString(),
    rawPayload: signal.rawPayload && typeof signal.rawPayload === 'object' && !Array.isArray(signal.rawPayload)
      ? signal.rawPayload
      : null
  };
}

function getSyncEligibleBotKeys(meta = {}) {
  const bots = Array.isArray(meta?.stats?.bots) ? meta.stats.bots : [];
  return bots
    .filter((bot) => bot && bot.syncEligible !== false && bot.key)
    .map((bot) => String(bot.key));
}

async function enqueueSnapshot(items, meta = {}) {
  const candidatesByDedupeKey = new Map();
  for (const item of items) {
    const signal = mapParserItemToSignal(item);
    const dedupeKey = signal.dedupeKey;
    const selected = pickBestCandidate(candidatesByDedupeKey.get(dedupeKey), { item, signal });
    candidatesByDedupeKey.set(dedupeKey, selected);
  }

  const selectedCandidates = Array.from(candidatesByDedupeKey.values());
  const payload = {
    providerId: config.providerId,
    items: selectedCandidates.map((entry) => serializeSignal(entry.signal)),
    meta: {
      timestamp: nowIso(),
      ...meta
    },
    syncEligibleBotKeys: getSyncEligibleBotKeys(meta)
  };

  const job = await signalsQueue.add('signals.snapshot', payload);

  return {
    queued: true,
    jobId: job.id,
    items: payload.items.length,
    syncEligibleBotKeys: payload.syncEligibleBotKeys,
    changedSignalIds: []
  };
}

async function syncSignalsSnapshotToDb(items, meta = {}) {
  return enqueueSnapshot(items, meta);
}

async function saveChangesToDb(changes, meta = {}) {
  const items = changes
    .map((change) => change.after || change.item)
    .filter(Boolean);

  if (items.length === 0) {
    return { queued: false, items: 0, changedSignalIds: [] };
  }

  return enqueueSnapshot(items, {
    ...meta,
    changesCount: changes.length,
    mode: 'incremental'
  });
}

async function logParserError(error, context = {}) {
  logger.error('Parser failure queued for external reporting', {
    timestamp: nowIso(),
    context,
    message: error?.message || String(error),
    stack: error?.stack || null
  });
}

async function closeStorage() {
  await signalsQueue.close();
  await redis.quit();
}

module.exports = {
  saveChangesToDb,
  syncSignalsSnapshotToDb,
  logParserError,
  closeStorage
};
