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

const TRACKED_IOR_KEYS = [
  'ior_mn',
  'ior_wmh_1',
  'ior_wmh_2',
  'ior_wmh_3',
  'ior_wmc_1',
  'ior_wmc_2',
  'ior_wmc_3',
  'ior_ot_1',
  'ior_ot_2',
  'ior_ot_3',
  'ior_ot_4',
  'ior_ot_5',
];

const TRACKED_IOR_SET = new Set(TRACKED_IOR_KEYS);

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 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 }) => {
  const eventId = +game?.eventId;
  const startTime = +game?.timestamp;
  const time = Date.now();

  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();
  let changed = false;
  const currentKeys = new Set(trackedEntries.map(([key]) => key));

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

  TRACKED_IOR_KEYS.forEach(key => {
    if (currentKeys.has(key) || !markets.has(key)) {
      return;
    }
    if (appendChangedPoint(markets, key, { 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;
  }
  return {
    ...history,
    markets: history.markets ? Object.fromEntries(Object.entries(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.values(history.markets ?? {}).filter(points => points?.length).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,
};
