import mongoose from 'mongoose';
const { Schema } = mongoose;

const TRACKED_IOR_KEYS = [
  'ior_ot_1',
  'ior_ot_2',
  'ior_ot_3',
  'ior_ot_4',
];

const TRACKED_IOR_SET = new Set(TRACKED_IOR_KEYS);
const TRACKED_PLATFORMS = ['pc', 'ob'];
const TRACKED_PLATFORM_SET = new Set(TRACKED_PLATFORMS);
const DEFAULT_PLATFORM = 'pc';

const ODDS_HISTORY_START_OFFSET = 2 * 60 * 60 * 1000;
const ODDS_HISTORY_END_OFFSET = 3 * 60 * 60 * 1000;
const ODDS_HISTORY_RETENTION = 3 * 24 * 60 * 60 * 1000;
const ODDS_HISTORY_CLEANUP_INTERVAL = 5 * 60 * 1000;

const oddsPointSchema = new Schema({
  time: { type: Number, required: true },
  value: { type: Number, required: true },
  source: { type: Number },
  origin: { type: String },
}, { _id: false });

const oddsHistorySchema = new Schema({
  eventId: { type: Number, required: true, unique: true, index: true },
  originId: { type: Number },
  leagueName: { type: String },
  teamHomeName: { type: String },
  teamAwayName: { type: String },
  startTime: { type: Number, required: true, index: true },
  endTime: { type: Number, required: true, index: true },
  markets: {
    type: Map,
    of: [oddsPointSchema],
    default: {},
  },
}, { timestamps: true });

const OddsHistory = mongoose.model('OddsHistory', oddsHistorySchema);

const isTrackedKey = (key) => TRACKED_IOR_SET.has(key);

const normalizePlatform = (platform) => {
  return TRACKED_PLATFORM_SET.has(platform) ? platform : DEFAULT_PLATFORM;
}

const getMarketKey = (platform, ior) => {
  return `${normalizePlatform(platform)}:${ior}`;
}

const parseMarketKey = (key) => {
  if (isTrackedKey(key)) {
    return { ior: key, platform: DEFAULT_PLATFORM };
  }
  const [platform, ior] = String(key).split(/[:.]/);
  if (TRACKED_PLATFORM_SET.has(platform) && isTrackedKey(ior)) {
    return { ior, platform };
  }
  return null;
}

const getMarketEntries = (markets = {}) => {
  if (markets instanceof Map) {
    return Array.from(markets.entries());
  }
  return Object.entries(markets);
}

const normalizeMarkets = (markets = {}) => {
  const normalized = {};
  getMarketEntries(markets).forEach(([key, points]) => {
    const parsed = parseMarketKey(key);
    if (!parsed || !points?.length) {
      return;
    }
    const marketKey = getMarketKey(parsed.platform, parsed.ior);
    normalized[marketKey] = [
      ...(normalized[marketKey] ?? []),
      ...points,
    ].sort((a, b) => a.time - b.time);
  });
  return normalized;
}

const migrateLegacyMarkets = (markets) => {
  getMarketEntries(markets).forEach(([key, points]) => {
    const parsed = parseMarketKey(key);
    if (!parsed || !points?.length) {
      return;
    }
    const marketKey = getMarketKey(parsed.platform, parsed.ior);
    if (key === marketKey) {
      return;
    }
    if (!markets.has(marketKey)) {
      markets.set(marketKey, points);
    }
    else {
      markets.set(marketKey, [
        ...markets.get(marketKey),
        ...points,
      ].sort((a, b) => a.time - b.time));
    }
    markets.delete(key);
  });
}

const isInsideTrackWindow = (eventTime, recordTime = Date.now()) => {
  if (!eventTime) {
    return false;
  }
  return recordTime >= eventTime - ODDS_HISTORY_START_OFFSET &&
    recordTime <= eventTime + ODDS_HISTORY_END_OFFSET;
}

const formatPoint = (event) => {
  const value = event?.v;
  if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
    return null;
  }
  return {
    value,
    source: event.s,
    origin: event.r,
  };
}

const appendChangedPoint = (markets, key, point, time) => {
  const history = markets.get(key) ?? [];
  const last = history[history.length - 1];
  if (last && last.value === point.value && last.source === point.source && last.origin === point.origin) {
    return false;
  }
  history.push({ time, ...point });
  markets.set(key, history);
  return true;
}

const recordGameOdds = async ({ game, events, platform = DEFAULT_PLATFORM }) => {
  const eventId = +game?.eventId;
  const startTime = +game?.timestamp;
  const time = Date.now();
  platform = normalizePlatform(platform);

  if (!eventId || !isInsideTrackWindow(startTime, time) || !events) {
    return { updated: false };
  }

  const trackedEntries = Object.entries(events)
    .filter(([key]) => isTrackedKey(key))
    .map(([key, event]) => [key, formatPoint(event)])
    .filter(([, point]) => point);

  const doc = await OddsHistory.findOne({ eventId });
  const markets = doc?.markets ?? new Map();
  migrateLegacyMarkets(markets);
  let changed = false;
  const currentKeys = new Set(trackedEntries.map(([key]) => getMarketKey(platform, key)));

  trackedEntries.forEach(([key, point]) => {
    if (appendChangedPoint(markets, getMarketKey(platform, key), point, time)) {
      changed = true;
    }
  });

  TRACKED_IOR_KEYS.forEach(key => {
    const marketKey = getMarketKey(platform, key);
    if (currentKeys.has(marketKey) || !markets.has(marketKey)) {
      return;
    }
    if (appendChangedPoint(markets, marketKey, { value: 0 }, time)) {
      changed = true;
    }
  });

  if (!changed) {
    return { updated: false };
  }

  await OddsHistory.findOneAndUpdate(
    { eventId },
    {
      $set: {
        originId: game.originId,
        leagueName: game.leagueName,
        teamHomeName: game.teamHomeName,
        teamAwayName: game.teamAwayName,
        startTime,
        endTime: startTime + ODDS_HISTORY_END_OFFSET,
        markets,
      },
    },
    { upsert: true, new: true },
  );

  return { updated: true };
}

const getGameOddsHistory = async (eventId) => {
  if (!eventId) {
    throw new Error('eventId is required');
  }
  const history = await OddsHistory.findOne({ eventId: +eventId }).lean({ flattenMaps: true });
  if (!history) {
    return null;
  }
  const markets = normalizeMarkets(history.markets);
  return {
    ...history,
    markets,
  };
}

const getOddsHistoryGames = async ({ page = 1, pageSize = 50, status = -1, keyword } = {}) => {
  page = Math.max(1, +page || 1);
  pageSize = Math.min(50, Math.max(1, +pageSize || 50));
  status = +status;

  const now = Date.now();
  const query = { eventId: { $gt: 0 } };
  if (status === 1) {
    query.endTime = { $gte: now };
  }
  else if (status === 2) {
    query.endTime = { $lt: now };
  }
  if (keyword) {
    const pattern = new RegExp(String(keyword).trim(), 'i');
    query.$or = [
      { leagueName: pattern },
      { teamHomeName: pattern },
      { teamAwayName: pattern },
    ];
    if (/^\d+$/.test(String(keyword).trim())) {
      query.$or.push({ eventId: +keyword });
    }
  }

  const [histories, total] = await Promise.all([
    OddsHistory.find(query)
    .select('eventId leagueName teamHomeName teamAwayName startTime endTime markets updatedAt')
    .sort({ startTime: -1 })
    .skip((page - 1) * pageSize)
    .limit(pageSize)
    .lean({ flattenMaps: true }),
    OddsHistory.countDocuments(query),
  ]);

  const list = histories.map(history => ({
    eventId: history.eventId,
    leagueName: history.leagueName,
    teamHomeName: history.teamHomeName,
    teamAwayName: history.teamAwayName,
    startTime: history.startTime,
    endTime: history.endTime,
    marketCount: Object.keys(normalizeMarkets(history.markets)).length,
    updatedAt: history.updatedAt,
  }));

  return { list, page, pageSize, total };
}

const cleanupExpiredHistory = async () => {
  const expireTime = Date.now() - ODDS_HISTORY_RETENTION;
  return OddsHistory.deleteMany({ startTime: { $lt: expireTime } });
}

const startCleanup = (logger = console) => {
  const cleanup = () => {
    cleanupExpiredHistory()
    .then(result => {
      if (result.deletedCount) {
        logger.out?.('odds history cleanup deleted %d records', result.deletedCount);
      }
    })
    .catch(err => {
      logger.out?.('odds history cleanup failed: %s', err.message);
    });
  };

  cleanup();
  return setInterval(cleanup, ODDS_HISTORY_CLEANUP_INTERVAL);
}

export {
  getGameOddsHistory,
  getOddsHistoryGames,
  recordGameOdds,
  startCleanup,
};

export default {
  getGameOddsHistory,
  getOddsHistoryGames,
  recordGameOdds,
  startCleanup,
};
