const MatrixRepository = require('../../repositories/matrix/MatrixRepository');
const Matrix = require('../../models/Matrix');

function isAdmin(user) {
  return !!user && ['admin', 'super_admin'].includes(user.role);
}

function isValidEmail(s) {
  return typeof s === 'string' && /\S+@\S+\.\S+/.test(s);
}

function toBool(value, defaultVal = false) {
  if (typeof value === 'boolean') return value;
  if (typeof value === 'string') {
    const v = value.trim().toLowerCase();
    if (v === 'true') return true;
    if (v === 'false') return false;
  }
  return defaultVal;
}

async function create({ name, topNodeEmail, actorUser, depth }) {
  if (!actorUser) {
    const err = new Error('Unauthorized');
    err.status = 401;
    throw err;
  }
  if (!isAdmin(actorUser)) {
    const err = new Error('Forbidden: Admins only');
    err.status = 403;
    throw err;
  }
  const trimmedName = (name || '').trim();
  if (!trimmedName) {
    const err = new Error('Matrix name is required');
    err.status = 400;
    throw err;
  }
  if (trimmedName.length > 255) {
    const err = new Error('Matrix name is too long');
    err.status = 400;
    throw err;
  }
  const email = (topNodeEmail || '').trim().toLowerCase();
  if (!isValidEmail(email)) {
    const err = new Error('Valid top node email is required');
    err.status = 400;
    throw err;
  }
  let maxDepth = undefined;
  if (depth !== undefined) {
    const d = Number(depth);
    if (!Number.isFinite(d) || d < 1 || d > 20) {
      const err = new Error('Matrix depth must be between 1 and 20');
      err.status = 400;
      throw err;
    }
    maxDepth = d;
  }

  const res = await MatrixRepository.createMatrix({ name: trimmedName, topNodeEmail: email, maxDepth });
  return new Matrix({
    name: res.name,
    masterTopUserId: res.masterTopUserId,
    masterTopUserEmail: res.masterTopUserEmail,
    rootUserId: res.rootUserId,
    matrixInstanceId: res.matrixInstanceId
  });
}

async function getStats({ actorUser }) {
  if (!actorUser) {
    const err = new Error('Unauthorized');
    err.status = 401;
    throw err;
  }
  if (!isAdmin(actorUser)) {
    const err = new Error('Forbidden: Admins only');
    err.status = 403;
    throw err;
  }

  const stats = await MatrixRepository.getMatrixStats();

  // Keep the response shape straightforward for the dashboard
  return {
    activeMatrices: stats.activeMatrices,
    totalMatrices: stats.totalMatrices,
    totalUsersSubscribed: stats.totalUsersSubscribed,
    matrices: stats.matrices.map(m => ({
      matrixInstanceId: Number(m.matrixInstanceId),
      rootUserId: Number(m.rootUserId),
      name: m.name,
      isActive: !!m.isActive,
      usersCount: m.usersCount,
      rogueUsersCount: m.rogueUsersCount,
      createdAt: m.createdAt,
      topNodeEmail: m.topNodeEmail,
      matrixId: Number(m.matrixInstanceId), // backward compatibility
      id: Number(m.matrixInstanceId)
    }))
  };
}

async function getUsers({ rootUserId, maxDepth, limit, offset, includeRoot, actorUser, matrixId, matrixInstanceId, topNodeEmail, rogueOnly }) {
  if (!actorUser) {
    const err = new Error('Unauthorized');
    err.status = 401;
    throw err;
  }
  if (!isAdmin(actorUser)) {
    const err = new Error('Forbidden: Admins only');
    err.status = 403;
    throw err;
  }

  const resolved = await MatrixRepository.resolveRootUserId({ rootUserId, matrixId, matrixInstanceId, topNodeEmail });
  const rid = resolved.rootUserId;
  const mid = resolved.matrixInstanceId;

  // NEW: do not clamp to 5 globally; repository will enforce per-root policy
  let depth = Number.parseInt(maxDepth ?? 5, 10);
  if (!Number.isFinite(depth)) depth = 5;
  if (depth < 0) depth = 0;

  let lim = Number.parseInt(limit ?? 100, 10);
  if (!Number.isFinite(lim) || lim <= 0) lim = 100;
  if (lim > 500) lim = 500;

  let off = Number.parseInt(offset ?? 0, 10);
  if (!Number.isFinite(off) || off < 0) off = 0;

  const incRoot = toBool(includeRoot, false);

  const users = await MatrixRepository.getMatrixUsers({
    rootUserId: rid,
    matrixInstanceId: mid,
    maxDepth: depth,
    limit: lim,
    offset: off,
    includeRoot: incRoot,
    rogueOnly: !!rogueOnly
  });

  return {
    matrixInstanceId: mid,
    rootUserId: rid,
    maxDepth: depth,
    limit: lim,
    offset: off,
    includeRoot: incRoot,
    rogueOnly: !!rogueOnly,
    users
  };
}

// NEW: search user candidates to add into a matrix
async function getUserCandidates({ q, type, limit, offset, rootUserId, matrixId, matrixInstanceId, topNodeEmail, actorUser }) {
  if (!actorUser) {
    const err = new Error('Unauthorized');
    err.status = 401;
    throw err;
  }
  if (!isAdmin(actorUser)) {
    const err = new Error('Forbidden: Admins only');
    err.status = 403;
    throw err;
  }

  const query = (q || '').trim();
  const normType = ['personal', 'company', 'all'].includes(String(type).toLowerCase())
    ? String(type).toLowerCase()
    : 'all';

  let lim = Number.parseInt(limit ?? 20, 10);
  if (!Number.isFinite(lim) || lim <= 0) lim = 20;
  if (lim > 50) lim = 50;

  let off = Number.parseInt(offset ?? 0, 10);
  if (!Number.isFinite(off) || off < 0) off = 0;

  // Return empty list when query too short
  if (query.length < 2) {
    return {
      q: query,
      type: normType,
      rootUserId: null,
      matrixInstanceId: null,          // ADDED for consistency
      limit: lim,
      offset: off,
      total: 0,
      items: []
    };
  }
  // Always resolve (covers legacy rootUserId only case)
  const { rootUserId: rid, matrixInstanceId: mid } = await MatrixRepository.resolveRootUserId({
    rootUserId,
    matrixId,
    matrixInstanceId,
    topNodeEmail
  });

  const { total, items } = await MatrixRepository.getUserSearchCandidates({
    q: query,
    type: normType,
    rootUserId: rid,
    matrixInstanceId: mid,
    limit: lim,
    offset: off
  });

  return {
    q: query,
    type: normType,
    rootUserId: rid,
    matrixInstanceId: mid,
    limit: lim,
    offset: off,
    total,
    items
  };
}

async function addUserToMatrix({
  rootUserId,
  matrixId,
  matrixInstanceId,
  topNodeEmail,
  childUserId,
  forceParentFallback,
  parentUserId,
  actorUser
}) {
  if (!actorUser) {
    const err = new Error('Unauthorized');
    err.status = 401;
    throw err;
  }
  if (!isAdmin(actorUser)) {
    const err = new Error('Forbidden: Admins only');
    err.status = 403;
    throw err;
  }
  const cId = Number(childUserId);
  if (!Number.isFinite(cId) || cId <= 0) {
    const err = new Error('Invalid childUserId');
    err.status = 400;
    throw err;
  }
  const fallback = toBool(forceParentFallback, false);
  const parentOverride = Number(parentUserId) > 0 ? Number(parentUserId) : undefined;

  const summary = await MatrixRepository.addUserToMatrix({
    rootUserId,
    matrixId,
    matrixInstanceId,
    topNodeEmail,
    childUserId: cId,
    forceParentFallback: fallback,         // respected by repository for root fallback
    parentUserId: parentOverride,
    actorUserId: actorUser.id
  });

  const users = await MatrixRepository.getMatrixUsers({
    rootUserId: summary.rootUserId,
    matrixInstanceId: summary.matrixInstanceId,
    maxDepth: 2,
    limit: 25,
    offset: 0,
    includeRoot: true
  });

  return {
    ...summary,
    usersPreview: users
  };
}

function maskName(name, email) {
  const safe = (s) => (typeof s === 'string' ? s.trim() : '');
  const n = safe(name);
  if (n) {
    const parts = n.split(/\s+/).filter(Boolean);
    if (parts.length === 1) {
      const fi = parts[0][0]?.toLowerCase() || '';
      return fi ? `${fi}…` : '…';
    }
    const first = parts[0][0]?.toLowerCase() || '';
    const last = parts[parts.length - 1][0]?.toLowerCase() || '';
    return `${first}… ${last}…`.trim();
  }
  const em = safe(email);
  if (em && em.includes('@')) {
    const [local, domain] = em.split('@');
    const fi = local?.[0]?.toLowerCase() || '';
    const li = domain?.[0]?.toLowerCase() || '';
    return `${fi}… ${li}…`.trim();
  }
  return '…';
}

// NEW: user-facing overview anchored at requester
async function getMyOverview({ userId, actorUser }) {
  if (!actorUser) {
    const err = new Error('Unauthorized');
    err.status = 401;
    throw err;
  }
  const uid = Number(userId);
  if (!Number.isFinite(uid) || uid <= 0) {
    const err = new Error('Invalid user');
    err.status = 400;
    throw err;
  }

  // Resolve the matrix instance that includes this user
  const resolved = await MatrixRepository.resolveInstanceForUser(uid);
  if (!resolved) {
    return {
      matrixInstanceId: null,
      totalUsersUnderMe: 0,
      levelsFilled: 0,
      immediateChildrenCount: 0,
      rootSlotsRemaining: null,
      level1: [],
      level2Plus: []
    };
  }
  const mid = resolved.matrixInstanceId;

  // Load instance policy and root user
  const instanceInfo = await MatrixRepository.getInstanceInfo(mid); // helper added below
  const rootUserId = Number(instanceInfo?.root_user_id || 0);
  const isRoot = rootUserId === uid;
  const policyDepth = instanceInfo?.max_depth == null
    ? (isRoot ? 20 : 5)
    : (isRoot ? Math.max(20, Number(instanceInfo.max_depth)) : Math.min(5, Number(instanceInfo.max_depth)));

  // Fetch descendants anchored at requester
  const users = await MatrixRepository.getMatrixUsers({
    rootUserId: uid,
    matrixInstanceId: mid,
    maxDepth: policyDepth,
    limit: 1000,
    offset: 0,
    includeRoot: false
  });

  // Bucket by depth
  const depthBuckets = new Map();
  for (const u of users) {
    const d = Number(u.depth);
    if (!depthBuckets.has(d)) depthBuckets.set(d, []);
    depthBuckets.get(d).push(u);
  }

  const immediateChildren = depthBuckets.get(1) || [];
  // Order level1 by position asc, then createdAt
  immediateChildren.sort((a, b) => {
    const pa = (a.position ?? 999999);
    const pb = (b.position ?? 999999);
    if (pa !== pb) return pa - pb;
    return (new Date(a.createdAt) - new Date(b.createdAt));
  });
  const level1 = immediateChildren.slice(0, 5).map(u => ({
    userId: u.userId,
    email: u.email,
    name: u.name || u.email,
    position: u.position ?? null
  }));

  const level2PlusRaw = users.filter(u => u.depth >= 2);
  const level2Plus = level2PlusRaw.map(u => ({
    userId: u.userId,
    depth: u.depth,
    name: maskName(u.name, u.email)
  }));

  // levelsFilled = highest depth having at least one user (bounded by policyDepth)
  let levelsFilled = 0;
  for (let d = 1; d <= policyDepth; d++) {
    if ((depthBuckets.get(d) || []).length > 0) levelsFilled = d;
  }

  // rootSlotsRemaining: only meaningful for root; compute among positions 1..5 under root
  let rootSlotsRemaining = null;
  if (isRoot) {
    const firstFiveUsed = immediateChildren
      .map(u => Number(u.position))
      .filter(p => Number.isFinite(p) && p >= 1 && p <= 5);
    const unique = new Set(firstFiveUsed);
    rootSlotsRemaining = Math.max(0, 5 - unique.size);
  }

  return {
    matrixInstanceId: mid,
    totalUsersUnderMe: users.length,
    levelsFilled,
    immediateChildrenCount: immediateChildren.length,
    rootSlotsRemaining,
    level1,
    level2Plus
  };
}

// NEW: list all matrices the user belongs to
async function listMyMatrices({ userId, actorUser }) {
  if (!actorUser) {
    const err = new Error('Unauthorized');
    err.status = 401;
    throw err;
  }
  const uid = Number(userId);
  if (!Number.isFinite(uid) || uid <= 0) {
    const err = new Error('Invalid user');
    err.status = 400;
    throw err;
  }

  const items = await MatrixRepository.listInstancesForUser(uid);

  // Enrich each item with summary (total users under requester, highest full level, fill percent)
  const enriched = [];
  for (const m of items) {
    const mid = Number(m.id ?? m.matrixInstanceId);
    // Load instance policy and root user
    const instanceInfo = await MatrixRepository.getInstanceInfo(mid);
    const policyDepth = instanceInfo?.max_depth == null
      ? (instanceInfo?.root_user_id === uid ? 20 : 5)
      : (instanceInfo?.root_user_id === uid ? Math.max(20, Number(instanceInfo.max_depth)) : Math.min(5, Number(instanceInfo.max_depth)));

    const users = await MatrixRepository.getMatrixUsers({
      rootUserId: uid,
      matrixInstanceId: mid,
      maxDepth: policyDepth,
      limit: 10000,
      offset: 0,
      includeRoot: false
    });

    const depthCounts = new Map();
    for (const u of users) {
      const d = Number(u.depth);
      depthCounts.set(d, (depthCounts.get(d) || 0) + 1);
    }

    const totalUsersUnderMe = users.length;

    let highestFullLevel = 0;
    for (let k = 1; k <= policyDepth; k++) {
      const theoretical = Math.pow(5, k);
      const actual = Number(depthCounts.get(k) || 0);
      if (actual === theoretical) {
        highestFullLevel = k;
      } else {
        break;
      }
    }

    let theoreticalTotal = 0;
    let occupiedTotal = 0;
    for (let k = 1; k <= policyDepth; k++) {
      const slots = Math.pow(5, k);
      theoreticalTotal += slots;
      occupiedTotal += Number(depthCounts.get(k) || 0);
    }
    const matrixFillPercent = theoreticalTotal > 0 ? Math.round((occupiedTotal / theoreticalTotal) * 10000) / 100 : 0;

    enriched.push({
      id: mid,
      matrixInstanceId: mid,
      name: String(m.name || `Matrix ${mid}`).trim(),
      totalUsersUnderMe,
      highestFullLevel,
      matrixFillPercent
    });
  }

  return enriched;
}

// NEW: personal overview for a specific matrix instance
async function getMyOverviewByInstance({ userId, matrixInstanceId, actorUser }) {
  if (!actorUser) {
    const err = new Error('Unauthorized');
    err.status = 401;
    throw err;
  }
  const uid = Number(userId);
  const mid = Number(matrixInstanceId);
  if (!Number.isFinite(uid) || uid <= 0) {
    const err = new Error('Invalid user');
    err.status = 400;
    throw err;
  }
  if (!Number.isFinite(mid) || mid <= 0) {
    const err = new Error('Invalid matrix instance id');
    err.status = 400;
    throw err;
  }

  // Ensure the user belongs to the requested instance
  const belongs = await MatrixRepository.userBelongsToInstance(uid, mid);
  if (!belongs) {
    const err = new Error('User does not belong to this matrix');
    err.status = 403;
    throw err;
  }

  // Load instance policy and root user
  const instanceInfo = await MatrixRepository.getInstanceInfo(mid);
  if (!instanceInfo) {
    const err = new Error('Matrix instance not found');
    err.status = 404;
    throw err;
  }
  const rootUserId = Number(instanceInfo?.root_user_id || 0);
  const isRoot = rootUserId === uid;
  const policyDepth = instanceInfo?.max_depth == null
    ? (isRoot ? 20 : 5)
    : (isRoot ? Math.max(20, Number(instanceInfo.max_depth)) : Math.min(5, Number(instanceInfo.max_depth)));

  // Fetch descendants anchored at requester within this instance
  const users = await MatrixRepository.getMatrixUsers({
    rootUserId: uid,
    matrixInstanceId: mid,
    maxDepth: policyDepth,
    limit: 1000,
    offset: 0,
    includeRoot: false
  });

  const depthBuckets = new Map();
  for (const u of users) {
    const d = Number(u.depth);
    if (!depthBuckets.has(d)) depthBuckets.set(d, []);
    depthBuckets.get(d).push(u);
  }

  const immediateChildren = depthBuckets.get(1) || [];
  immediateChildren.sort((a, b) => {
    const pa = (a.position ?? 999999);
    const pb = (b.position ?? 999999);
    if (pa !== pb) return pa - pb;
    return (new Date(a.createdAt) - new Date(b.createdAt));
  });
  const level1 = immediateChildren.slice(0, 5).map(u => ({
    userId: u.userId,
    email: u.email,
    name: u.name || u.email,
    position: u.position ?? null
  }));

  const level2PlusRaw = users.filter(u => u.depth >= 2);
  const level2Plus = level2PlusRaw.map(u => ({
    userId: u.userId,
    depth: u.depth,
    name: maskName(u.name, u.email)
  }));

  let levelsFilled = 0;
  for (let d = 1; d <= policyDepth; d++) {
    if ((depthBuckets.get(d) || []).length > 0) levelsFilled = d;
  }

  let rootSlotsRemaining = null;
  if (isRoot) {
    const firstFiveUsed = immediateChildren
      .map(u => Number(u.position))
      .filter(p => Number.isFinite(p) && p >= 1 && p <= 5);
    const unique = new Set(firstFiveUsed);
    rootSlotsRemaining = Math.max(0, 5 - unique.size);
  }

  return {
    matrixInstanceId: mid,
    totalUsersUnderMe: users.length,
    levelsFilled,
    immediateChildrenCount: immediateChildren.length,
    rootSlotsRemaining,
    level1,
    level2Plus
  };
}

// NEW: personal matrix summary (totals and fill) for a specific instance
async function getMyMatrixSummary({ userId, matrixInstanceId, actorUser }) {
  if (!actorUser) {
    const err = new Error('Unauthorized');
    err.status = 401;
    throw err;
  }
  const uid = Number(userId);
  const mid = Number(matrixInstanceId);
  if (!Number.isFinite(uid) || uid <= 0) {
    const err = new Error('Invalid user');
    err.status = 400;
    throw err;
  }
  if (!Number.isFinite(mid) || mid <= 0) {
    const err = new Error('Invalid matrix instance id');
    err.status = 400;
    throw err;
  }

  // Ensure membership
  const belongs = await MatrixRepository.userBelongsToInstance(uid, mid);
  if (!belongs) {
    const err = new Error('User does not belong to this matrix');
    err.status = 403;
    throw err;
  }

  // Policy info
  const instanceInfo = await MatrixRepository.getInstanceInfo(mid);
  if (!instanceInfo) {
    const err = new Error('Matrix instance not found');
    err.status = 404;
    throw err;
  }
  const policyDepth = instanceInfo?.max_depth == null ? (instanceInfo?.root_user_id === uid ? 20 : 5) : (instanceInfo?.root_user_id === uid ? Math.max(20, Number(instanceInfo.max_depth)) : Math.min(5, Number(instanceInfo.max_depth)));

  // Fetch all descendants anchored at requester within this instance
  const users = await MatrixRepository.getMatrixUsers({
    rootUserId: uid,
    matrixInstanceId: mid,
    maxDepth: policyDepth,
    limit: 10000,
    offset: 0,
    includeRoot: false
  });

  // Bucket counts by depth
  const depthCounts = new Map(); // depth -> count
  for (const u of users) {
    const d = Number(u.depth);
    depthCounts.set(d, (depthCounts.get(d) || 0) + 1);
  }

  const totalUsersUnderMe = users.length;

  // highestFullLevel: largest k where count at depth k equals 5^k (bounded by policyDepth)
  let highestFullLevel = 0;
  for (let k = 1; k <= policyDepth; k++) {
    const theoretical = Math.pow(5, k);
    const actual = Number(depthCounts.get(k) || 0);
    if (actual === theoretical) {
      highestFullLevel = k;
    } else {
      break; // stop at first non-full level
    }
  }

  // matrixFillPercent: occupied slots / theoretical slots across depths 1..policyDepth
  let theoreticalTotal = 0;
  let occupiedTotal = 0;
  for (let k = 1; k <= policyDepth; k++) {
    const slots = Math.pow(5, k);
    theoreticalTotal += slots;
    occupiedTotal += Number(depthCounts.get(k) || 0);
  }
  const matrixFillPercent = theoreticalTotal > 0 ? Math.round((occupiedTotal / theoreticalTotal) * 10000) / 100 : 0;

  return {
    matrixInstanceId: mid,
    totalUsersUnderMe,
    highestFullLevel,
    matrixFillPercent
  };
}

async function deactivate({ matrixInstanceId, matrixId, rootUserId, topNodeEmail, actorUser }) {
  if (!actorUser) {
    const err = new Error('Unauthorized');
    err.status = 401;
    throw err;
  }
  if (!isAdmin(actorUser)) {
    const err = new Error('Forbidden: Admins only');
    err.status = 403;
    throw err;
  }

  let instanceId = Number.parseInt(matrixInstanceId ?? matrixId, 10);
  if (!Number.isFinite(instanceId) || instanceId <= 0) {
    // Try to resolve via other hints
    const resolved = await MatrixRepository.resolveRootUserId({
      rootUserId,
      matrixId,
      matrixInstanceId,
      topNodeEmail
    });
    instanceId = resolved.matrixInstanceId;
  }

  const res = await MatrixRepository.deactivateInstance(instanceId);
  return {
    matrixInstanceId: res.matrixInstanceId,
    wasActive: res.wasActive,
    isActive: res.isActive,
    status: res.status
  };
}

// NEW: activate a matrix instance (admin-only)
async function activate({ matrixInstanceId, matrixId, rootUserId, topNodeEmail, actorUser }) {
  if (!actorUser) {
    const err = new Error('Unauthorized');
    err.status = 401;
    throw err;
  }
  if (!isAdmin(actorUser)) {
    const err = new Error('Forbidden: Admins only');
    err.status = 403;
    throw err;
  }

  let instanceId = Number.parseInt(matrixInstanceId ?? matrixId, 10);
  if (!Number.isFinite(instanceId) || instanceId <= 0) {
    const resolved = await MatrixRepository.resolveRootUserId({
      rootUserId,
      matrixId,
      matrixInstanceId,
      topNodeEmail
    });
    instanceId = resolved.matrixInstanceId;
  }

  const res = await MatrixRepository.activateInstance(instanceId);
  return {
    matrixInstanceId: res.matrixInstanceId,
    wasActive: res.wasActive,
    isActive: res.isActive,
    status: res.status
  };
}

// NEW: remove user and create vacancy (admin)
async function removeUser({ matrixInstanceId, userId, actorUser }) {
  if (!actorUser) { const err = new Error('Unauthorized'); err.status = 401; throw err; }
  if (!isAdmin(actorUser)) { const err = new Error('Forbidden: Admins only'); err.status = 403; throw err; }
  const mid = Number(matrixInstanceId);
  const uid = Number(userId);
  if (!Number.isFinite(mid) || mid <= 0 || !Number.isFinite(uid) || uid <= 0) {
    const err = new Error('Invalid parameters');
    err.status = 400;
    throw err;
  }
  return await MatrixRepository.removeUserAndCreateVacancy(mid, uid);
}

// NEW: assign user to vacancy (admin)
async function assignVacancy({ matrixInstanceId, parentUserId, position, userId, actorUser }) {
  if (!actorUser) { const err = new Error('Unauthorized'); err.status = 401; throw err; }
  if (!isAdmin(actorUser)) { const err = new Error('Forbidden: Admins only'); err.status = 403; throw err; }
  return await MatrixRepository.assignUserToVacancy({ matrixInstanceId, parentUserId, position, userId });
}

// NEW: list vacancies (admin)
async function listVacancies({ matrixInstanceId, actorUser }) {
  if (!actorUser) { const err = new Error('Unauthorized'); err.status = 401; throw err; }
  if (!isAdmin(actorUser)) { const err = new Error('Forbidden: Admins only'); err.status = 403; throw err; }
  return await MatrixRepository.listVacancies(matrixInstanceId);
}

module.exports = {
  create,
  getStats,
  getUsers,
  getUserCandidates,
  addUserToMatrix,
  getMyOverview, // NEW
  listMyMatrices, // NEW (enhanced)
  getMyOverviewByInstance, // NEW
  getMyMatrixSummary, // NEW
  deactivate, // NEW
  activate, // NEW
  removeUser, // NEW
  assignVacancy, // NEW
  listVacancies // NEW
};
