import { Types } from 'mongoose';
import SyncSession from '../models/syncSession';
import SyncTrackingEvent from '../models/syncTrackingEvent';
import SyncResourceUsage from '../models/syncResourceUsage';
import SyncSessionPeriod from '../models/syncSessionPeriod';
import User from '../models/users';

const err = (msg: string, code = 400): Error & { statusCode: number } =>
  Object.assign(new Error(msg), { statusCode: code });

// local_id is a per-device SQLite autoincrement counter on the client, not
// globally unique per employee (a reinstall or second device restarts it at
// 1) — fold in device_name so two devices reusing the same local_id never
// collide into one record. Missing device_name is normalized to a stable
// value so the same "unknown" bucket is used consistently on lookup too.
const deviceKey = (name?: string): string => (name && name.trim() ? name : 'unknown-device');

interface SyncIdMap {
  local_id: number;
  server_id: string;
}

interface RawSessionInput {
  id: number;
  login_time: string;
  logout_time?: string | null;
  total_work_seconds?: number;
  total_break_seconds?: number;
  total_idle_seconds?: number;
  status?: string;
  device_name?: string;
  operating_system?: string;
  ip_address?: string;
}

interface RawTrackingEventInput {
  id: number;
  session_id?: number | null;
  event_type: string;
  event_time: string;
  device_name?: string;
  operating_system?: string;
  ip_address?: string;
  metadata?: unknown;
}

interface RawResourceUsageInput {
  id: number;
  session_id?: number | null;
  cpu_usage_percent?: number;
  ram_usage_mb?: number;
  ram_usage_percent?: number;
  disk_read_speed_kbps?: number;
  disk_write_speed_kbps?: number;
  network_upload_speed_kbps?: number;
  network_download_speed_kbps?: number;
  total_memory_mb?: number;
  available_memory_mb?: number;
  device_name?: string;
  operating_system?: string;
  recorded_at: string;
}

interface RawSessionPeriodInput {
  id: number;
  session_id?: number | null;
  type: string;
  start_time: string;
  end_time?: string | null;
  duration_seconds?: number;
  duration_minutes?: number;
  remarks?: string | null;
}

export interface SyncPayload {
  tracking_events?: RawTrackingEventInput[];
  sessions?: RawSessionInput[];
  resource_usage?: RawResourceUsageInput[];
  session_periods?: RawSessionPeriodInput[];
}

// ─── Sync (upsert on user + client local id) ──────────────────────────────────

export const syncDesktopData = async (userId: string, payload: SyncPayload) => {
  const sessions = payload.sessions ?? [];
  const trackingEvents = payload.tracking_events ?? [];
  const resourceUsage = payload.resource_usage ?? [];
  const sessionPeriods = payload.session_periods ?? [];

  // Sessions must be resolved first so events/usage/periods can link to the
  // server-side session _id via the client's local session id. Keyed by
  // device+local_id since local_id alone can collide across devices.
  const sessionIdMap = new Map<string, Types.ObjectId>();

  const syncedSessions: SyncIdMap[] = [];
  for (const s of sessions) {
    const device = deviceKey(s.device_name);
    const doc = await SyncSession.findOneAndUpdate(
      { user: userId, device_name: device, local_id: s.id },
      {
        user: userId,
        local_id: s.id,
        login_time: new Date(s.login_time),
        logout_time: s.logout_time ? new Date(s.logout_time) : null,
        total_work_seconds: s.total_work_seconds ?? 0,
        total_break_seconds: s.total_break_seconds ?? 0,
        total_idle_seconds: s.total_idle_seconds ?? 0,
        status: s.status,
        device_name: device,
        operating_system: s.operating_system,
        ip_address: s.ip_address,
      },
      { upsert: true, new: true, setDefaultsOnInsert: true },
    );
    sessionIdMap.set(`${device}::${s.id}`, doc._id as Types.ObjectId);
    syncedSessions.push({ local_id: s.id, server_id: doc._id.toString() });
  }

  // deviceName should be the same device that reported the event/usage/period
  // (they carry their own device_name); without one there's no way to
  // disambiguate and the lookup falls back to "first match for this user".
  const resolveSession = async (
    localSessionId?: number | null,
    deviceName?: string,
  ): Promise<Types.ObjectId | null> => {
    if (localSessionId === undefined || localSessionId === null) return null;
    const device = deviceName ? deviceKey(deviceName) : undefined;
    if (device) {
      const fromBatch = sessionIdMap.get(`${device}::${localSessionId}`);
      if (fromBatch) return fromBatch;
    }
    // Session may have been synced in an earlier batch, not this one.
    const existing = await SyncSession.findOne({
      user: userId,
      local_id: localSessionId,
      ...(device ? { device_name: device } : {}),
    })
      .select('_id')
      .lean();
    return existing ? (existing._id as Types.ObjectId) : null;
  };

  const syncedTrackingEvents: SyncIdMap[] = [];
  for (const e of trackingEvents) {
    const session = await resolveSession(e.session_id, e.device_name);
    const doc = await SyncTrackingEvent.findOneAndUpdate(
      { user: userId, session, local_id: e.id },
      {
        user: userId,
        local_id: e.id,
        session,
        local_session_id: e.session_id ?? undefined,
        event_type: e.event_type,
        event_time: new Date(e.event_time),
        device_name: e.device_name,
        operating_system: e.operating_system,
        ip_address: e.ip_address,
        metadata: e.metadata ?? null,
      },
      { upsert: true, new: true, setDefaultsOnInsert: true },
    );
    syncedTrackingEvents.push({ local_id: e.id, server_id: doc._id.toString() });
  }

  const syncedResourceUsage: SyncIdMap[] = [];
  for (const r of resourceUsage) {
    const session = await resolveSession(r.session_id, r.device_name);
    const doc = await SyncResourceUsage.findOneAndUpdate(
      { user: userId, session, local_id: r.id },
      {
        user: userId,
        local_id: r.id,
        session,
        local_session_id: r.session_id ?? undefined,
        cpu_usage_percent: r.cpu_usage_percent,
        ram_usage_mb: r.ram_usage_mb,
        ram_usage_percent: r.ram_usage_percent,
        disk_read_speed_kbps: r.disk_read_speed_kbps,
        disk_write_speed_kbps: r.disk_write_speed_kbps,
        network_upload_speed_kbps: r.network_upload_speed_kbps,
        network_download_speed_kbps: r.network_download_speed_kbps,
        total_memory_mb: r.total_memory_mb,
        available_memory_mb: r.available_memory_mb,
        device_name: r.device_name,
        operating_system: r.operating_system,
        recorded_at: new Date(r.recorded_at),
      },
      { upsert: true, new: true, setDefaultsOnInsert: true },
    );
    syncedResourceUsage.push({ local_id: r.id, server_id: doc._id.toString() });
  }

  const syncedSessionPeriods: SyncIdMap[] = [];
  for (const p of sessionPeriods) {
    // session_periods carries no device_name of its own, so this can't be
    // disambiguated the same way — falls back to first match for this user.
    const session = await resolveSession(p.session_id);
    const doc = await SyncSessionPeriod.findOneAndUpdate(
      { user: userId, session, local_id: p.id },
      {
        user: userId,
        local_id: p.id,
        session,
        local_session_id: p.session_id ?? undefined,
        type: p.type,
        start_time: new Date(p.start_time),
        end_time: p.end_time ? new Date(p.end_time) : null,
        duration_seconds: p.duration_seconds,
        duration_minutes: p.duration_minutes,
        remarks: p.remarks ?? null,
      },
      { upsert: true, new: true, setDefaultsOnInsert: true },
    );
    syncedSessionPeriods.push({ local_id: p.id, server_id: doc._id.toString() });
  }

  return {
    synced_sessions: syncedSessions,
    synced_tracking_events: syncedTrackingEvents,
    synced_resource_usage: syncedResourceUsage,
    synced_session_periods: syncedSessionPeriods,
  };
};

// ─── Read ──────────────────────────────────────────────────────────────────

interface SyncQuery {
  limit?: number;
}

const clampLimit = (limit?: number): number => Math.min(500, Math.max(1, limit || 100));

export const getSyncDataForUser = async (userId: string, query: SyncQuery = {}) => {
  const limit = clampLimit(query.limit);

  const [sessions, trackingEvents, resourceUsage, sessionPeriods] = await Promise.all([
    SyncSession.find({ user: userId }).sort({ login_time: -1 }).limit(limit).lean(),
    SyncTrackingEvent.find({ user: userId }).sort({ event_time: -1 }).limit(limit).lean(),
    SyncResourceUsage.find({ user: userId }).sort({ recorded_at: -1 }).limit(limit).lean(),
    SyncSessionPeriod.find({ user: userId }).sort({ start_time: -1 }).limit(limit).lean(),
  ]);

  return { sessions, tracking_events: trackingEvents, resource_usage: resourceUsage, session_periods: sessionPeriods };
};

export const getSyncDataForEmployee = async (employeeId: string, query: SyncQuery = {}) => {
  if (!Types.ObjectId.isValid(employeeId)) throw err('Invalid employee_id');
  const employee = await User.findById(employeeId).select('name email department').lean();
  if (!employee) throw err('Employee not found', 404);

  const data = await getSyncDataForUser(employeeId, query);
  return { employee, ...data };
};

// ─── Patch (checkpoint an in-progress session) ────────────────────────────────

interface SessionPatchInput {
  logout_time?: string | null;
  total_work_seconds?: number;
  total_break_seconds?: number;
  total_idle_seconds?: number;
  status?: string;
}

export const patchSyncSession = async (
  userId: string,
  localSessionId: number,
  updates: SessionPatchInput,
) => {
  const session = await SyncSession.findOne({ user: userId, local_id: localSessionId });
  if (!session) throw err('Session not found. Sync it first via POST /api/desktop/sync', 404);

  if (updates.logout_time !== undefined)
    session.logout_time = updates.logout_time ? new Date(updates.logout_time) : null;
  if (updates.total_work_seconds !== undefined) session.total_work_seconds = updates.total_work_seconds;
  if (updates.total_break_seconds !== undefined) session.total_break_seconds = updates.total_break_seconds;
  if (updates.total_idle_seconds !== undefined) session.total_idle_seconds = updates.total_idle_seconds;
  if (updates.status !== undefined) session.status = updates.status;

  await session.save();
  return session;
};
