const fs = require('fs');
const path = require('path');
const crypto = require('crypto');

const TASK_ATTACHMENTS_DIR = 'task-attachments';
const MAX_TASK_ATTACHMENT_BYTES = 20 * 1024 * 1024;
const TEAM_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,127}$/;
const LEAD_AGENT_TYPES = new Set(['team-lead', 'lead', 'orchestrator']);
const CROSS_TEAM_TOOL_RECIPIENT_NAMES = new Set([
  'cross_team_send',
  'cross_team_list_targets',
  'cross_team_get_outbox',
]);

function nowIso() {
  return new Date().toISOString();
}

function makeId() {
  return crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`;
}

function ensureDir(dirPath) {
  fs.mkdirSync(dirPath, { recursive: true });
}

function readJson(filePath, fallbackValue) {
  try {
    return JSON.parse(fs.readFileSync(filePath, 'utf8'));
  } catch (error) {
    if (error && error.code === 'ENOENT') {
      return fallbackValue;
    }
    throw error;
  }
}

function isSafePathSegment(value) {
  const normalized = String(value == null ? '' : value);
  if (normalized.length === 0 || normalized.trim().length === 0) return false;
  if (normalized === '.' || normalized === '..') return false;
  if (normalized.includes('/') || normalized.includes('\\')) return false;
  if (normalized.includes('..')) return false;
  if (normalized.includes('\0')) return false;
  return true;
}

function assertSafePathSegment(label, value) {
  const normalized = String(value == null ? '' : value);
  if (!isSafePathSegment(normalized)) {
    throw new Error(`Invalid ${String(label)}`);
  }
  return normalized;
}

function looksLikeQualifiedExternalRecipient(name) {
  const trimmed = String(name || '').trim();
  const dot = trimmed.indexOf('.');
  if (dot <= 0 || dot === trimmed.length - 1) return false;
  const teamName = trimmed.slice(0, dot).trim();
  const memberName = trimmed.slice(dot + 1).trim();
  return TEAM_NAME_PATTERN.test(teamName) && memberName.length > 0;
}

function looksLikeCrossTeamPseudoRecipient(name) {
  const trimmed = String(name || '').trim();
  const prefixes = [
    'cross_team::',
    'cross_team--',
    'cross-team:',
    'cross-team-',
    'cross_team:',
    'cross_team-',
  ];
  for (const prefix of prefixes) {
    if (!trimmed.startsWith(prefix)) continue;
    const teamName = trimmed.slice(prefix.length).trim();
    if (TEAM_NAME_PATTERN.test(teamName)) {
      return true;
    }
  }
  return false;
}

function looksLikeCrossTeamToolRecipient(name) {
  return CROSS_TEAM_TOOL_RECIPIENT_NAMES.has(String(name || '').trim());
}

function looksLikeCrossTeamRecipient(name) {
  return looksLikeQualifiedExternalRecipient(name) || looksLikeCrossTeamPseudoRecipient(name);
}

function getHomeDir() {
  if (process.env.HOME) return process.env.HOME;
  if (process.env.USERPROFILE) return process.env.USERPROFILE;
  if (process.env.HOMEDRIVE && process.env.HOMEPATH) {
    return process.env.HOMEDRIVE + process.env.HOMEPATH;
  }
  try {
    return require('os').homedir();
  } catch {
    return '';
  }
}

function getClaudeDir(flags) {
  const explicit =
    (typeof flags['claude-dir'] === 'string' && flags['claude-dir']) ||
    (typeof flags.claudeDir === 'string' && flags.claudeDir) ||
    (typeof flags.claude_path === 'string' && flags.claude_path) ||
    '';
  if (explicit) {
    return path.resolve(explicit);
  }
  const home = getHomeDir();
  if (!home) {
    throw new Error('HOME/USERPROFILE is not set');
  }
  return path.join(home, '.claude');
}

function getPaths(flags, teamName) {
  const claudeDir = getClaudeDir(flags);
  const safeTeam = assertSafePathSegment('team', teamName);
  const teamDir = path.join(claudeDir, 'teams', safeTeam);
  const tasksDir = path.join(claudeDir, 'tasks', safeTeam);
  const kanbanPath = path.join(teamDir, 'kanban-state.json');
  const processesPath = path.join(teamDir, 'processes.json');
  return { claudeDir, teamDir, tasksDir, kanbanPath, processesPath };
}

function isCanonicalLeadMember(member) {
  if (!member || typeof member !== 'object') return false;
  const agentType = typeof member.agentType === 'string' ? member.agentType.trim().toLowerCase() : '';
  const role = typeof member.role === 'string' ? member.role.trim().toLowerCase() : '';
  const name = typeof member.name === 'string' ? member.name.trim().toLowerCase() : '';
  return (
    LEAD_AGENT_TYPES.has(agentType) ||
    name === 'team-lead' ||
    role === 'team-lead' ||
    role === 'team lead' ||
    role === 'lead'
  );
}

function normalizeMemberKey(value) {
  return typeof value === 'string' && value.trim() ? value.trim().toLowerCase() : '';
}

function collectExplicitTeamMembers(paths) {
  const config = readTeamConfig(paths) || {};
  const configMembers = Array.isArray(config.members) ? config.members : [];
  const metaMembers = readMembersMeta(paths);
  const membersByKey = new Map();
  const removedNames = new Set();

  for (const rawMember of configMembers) {
    const normalized = normalizeMemberRecord(rawMember);
    if (!normalized) continue;
    membersByKey.set(normalizeMemberKey(normalized.name), normalized);
  }

  for (const rawMember of metaMembers) {
    const normalized = normalizeMemberRecord(rawMember);
    if (!normalized) continue;
    const key = normalizeMemberKey(normalized.name);
    if (normalized.removedAt != null) {
      membersByKey.delete(key);
      removedNames.add(key);
      continue;
    }
    removedNames.delete(key);
    membersByKey.set(key, mergeResolvedMember(membersByKey.get(key) || { name: normalized.name }, normalized));
  }

  return { membersByKey, removedNames };
}

function inferLeadName(paths) {
  const resolved = resolveTeamMembers(paths);
  const members = resolved.members || [];
  const lead =
    members.find((member) => {
      const agentType = typeof member?.agentType === 'string' ? member.agentType.trim().toLowerCase() : '';
      return LEAD_AGENT_TYPES.has(agentType);
    }) ||
    members.find((member) => String((member && member.name) || '').trim().toLowerCase() === 'team-lead') ||
    members.find(
      (member) => {
        const role = typeof member.role === 'string' ? member.role.trim().toLowerCase() : '';
        return role === 'team-lead' || role === 'team lead' || role === 'lead';
      }
    );
  if (lead) {
    return String(lead.name);
  }
  const config = resolved.config;
  if (config && Array.isArray(config.members) && config.members[0]) {
    return String(config.members[0].name);
  }
  return 'team-lead';
}

function resolveExplicitTeamMemberName(paths, candidate, options = {}) {
  const normalized = typeof candidate === 'string' && candidate.trim() ? candidate.trim() : '';
  const key = normalizeMemberKey(normalized);
  if (!key) return null;

  const explicit = collectExplicitTeamMembers(paths);
  if (explicit.removedNames.has(key)) return null;
  const directMember = explicit.membersByKey.get(key);
  if (directMember) {
    return directMember.name;
  }

  if (options.allowLeadAliases !== false) {
    const leadName = inferLeadName(paths);
    const leadKey = normalizeMemberKey(leadName);
    if (key === 'lead' || key === 'team-lead' || (leadKey && key === leadKey)) {
      const leadMember = leadKey ? explicit.membersByKey.get(leadKey) : null;
      return leadMember ? leadMember.name : null;
    }
  }

  return null;
}

function getLeadProviderKeys(paths, explicitMembers) {
  const leadName = inferLeadName(paths);
  const leadKey = normalizeMemberKey(leadName);
  const leadMember =
    (leadKey ? explicitMembers.membersByKey.get(leadKey) : null) ||
    Array.from(explicitMembers.membersByKey.values()).find((member) => isCanonicalLeadMember(member));
  if (!leadMember) return { leadName: '', keys: new Set() };

  const keys = new Set();
  for (const field of ['providerId', 'provider', 'providerBackendId']) {
    const key = normalizeMemberKey(leadMember[field]);
    if (key) keys.add(key);
  }
  return { leadName: leadMember.name, keys };
}

function formatAllowedTaskCommentAuthors(paths, explicitMembers, options = {}) {
  const allowed = new Set();
  if (options.allowReservedAuthors !== false) {
    allowed.add('user');
    allowed.add('system');
  }
  for (const member of explicitMembers.membersByKey.values()) {
    if (member && typeof member.name === 'string' && member.name.trim()) {
      allowed.add(member.name.trim());
    }
  }

  if (options.allowLeadAliases !== false) {
    const leadName = inferLeadName(paths);
    const leadKey = normalizeMemberKey(leadName);
    if (leadKey && explicitMembers.membersByKey.has(leadKey)) {
      allowed.add('lead');
      allowed.add('team-lead');
    }
  }

  return Array.from(allowed).sort((a, b) => a.localeCompare(b)).join(', ');
}

function resolveTaskCommentAuthorName(paths, candidate, label = 'task comment author', options = {}) {
  const normalized = typeof candidate === 'string' && candidate.trim() ? candidate.trim() : '';
  if (!normalized) {
    return inferLeadName(paths);
  }

  const key = normalizeMemberKey(normalized);
  if (key === 'user' || key === 'system') {
    if (options.allowReservedAuthors === false) {
      throw new Error(
        `${label} "${key}" is reserved for app-owned writes. Set from to your configured teammate name.`
      );
    }
    return key;
  }

  const explicit = collectExplicitTeamMembers(paths);
  const directMember = explicit.membersByKey.get(key);
  if (directMember && !explicit.removedNames.has(key)) {
    return directMember.name;
  }

  const leadAlias = resolveExplicitTeamMemberName(paths, normalized, {
    allowLeadAliases: options.allowLeadAliases !== false,
  });
  if (leadAlias) {
    return leadAlias;
  }

  if (options.allowProviderAliases !== false) {
    const { leadName, keys } = getLeadProviderKeys(paths, explicit);
    if (leadName && keys.has(key)) {
      return leadName;
    }
  }

  throw new Error(
    `Unknown ${label}: ${normalized}. Use one of: ${formatAllowedTaskCommentAuthors(
      paths,
      explicit,
      options
    )}.`
  );
}

function assertExplicitTeamMemberName(paths, candidate, label = 'member', options = {}) {
  const resolved = resolveExplicitTeamMemberName(paths, candidate, options);
  if (!resolved) {
    const value = typeof candidate === 'string' && candidate.trim() ? candidate.trim() : String(candidate || '');
    throw new Error(`Unknown ${label}: ${value}. Use a configured team member name.`);
  }
  return resolved;
}

function readTeamConfig(paths) {
  return readJson(path.join(paths.teamDir, 'config.json'), null);
}

function readMembersMeta(paths) {
  let parsed;
  try {
    parsed = readJson(path.join(paths.teamDir, 'members.meta.json'), null);
  } catch {
    return [];
  }
  if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.members)) {
    return [];
  }
  return parsed.members.filter((member) => member && typeof member === 'object');
}

function listInboxMemberNames(paths) {
  const inboxDir = path.join(paths.teamDir, 'inboxes');
  let entries;
  try {
    entries = fs.readdirSync(inboxDir, { withFileTypes: true });
  } catch {
    return [];
  }

  return entries
    .filter((entry) => entry && entry.isFile() && entry.name.endsWith('.json'))
    .map((entry) => entry.name.slice(0, -5))
    .map((name) => String(name || '').trim())
    .filter((name) => name && name !== 'user')
    .filter((name) => !looksLikeCrossTeamPseudoRecipient(name))
    .filter((name) => !looksLikeCrossTeamToolRecipient(name));
}

function normalizeMemberRecord(member) {
  if (!member || typeof member !== 'object') return null;
  const name = typeof member.name === 'string' ? member.name.trim() : '';
  if (!name) return null;
  const copyTrimmedString = (key) =>
    typeof member[key] === 'string' && member[key].trim()
      ? { [key]: member[key].trim() }
      : {};
  return {
    name,
    ...(typeof member.role === 'string' && member.role.trim() ? { role: member.role.trim() } : {}),
    ...(typeof member.workflow === 'string' && member.workflow.trim()
      ? { workflow: member.workflow.trim() }
      : {}),
    ...(typeof member.agentType === 'string' && member.agentType.trim()
      ? { agentType: member.agentType.trim() }
      : {}),
    ...(typeof member.color === 'string' && member.color.trim() ? { color: member.color.trim() } : {}),
    ...(typeof member.cwd === 'string' && member.cwd.trim() ? { cwd: member.cwd.trim() } : {}),
    ...copyTrimmedString('providerId'),
    ...copyTrimmedString('providerBackendId'),
    ...copyTrimmedString('provider'),
    ...copyTrimmedString('model'),
    ...copyTrimmedString('effort'),
    ...copyTrimmedString('fastMode'),
    ...(typeof member.removedAt === 'number' ? { removedAt: member.removedAt } : {}),
  };
}

function mergeResolvedMember(target, source) {
  if (!source) return target;
  return {
    ...target,
    ...(source.name ? { name: source.name } : {}),
    ...(source.role ? { role: source.role } : {}),
    ...(source.workflow ? { workflow: source.workflow } : {}),
    ...(source.agentType ? { agentType: source.agentType } : {}),
    ...(source.color ? { color: source.color } : {}),
    ...(source.cwd ? { cwd: source.cwd } : {}),
    ...(source.providerId ? { providerId: source.providerId } : {}),
    ...(source.providerBackendId ? { providerBackendId: source.providerBackendId } : {}),
    ...(source.provider ? { provider: source.provider } : {}),
    ...(source.model ? { model: source.model } : {}),
    ...(source.effort ? { effort: source.effort } : {}),
    ...(source.fastMode ? { fastMode: source.fastMode } : {}),
    ...(source.removedAt != null ? { removedAt: source.removedAt } : {}),
  };
}

function resolveTeamMembers(paths) {
  const config = readTeamConfig(paths) || {};
  const configMembers = Array.isArray(config.members) ? config.members : [];
  const metaMembers = readMembersMeta(paths);
  const inboxNames = listInboxMemberNames(paths);
  const memberMap = new Map();
  const removedNames = new Set();

  for (const rawMember of configMembers) {
    const normalized = normalizeMemberRecord(rawMember);
    if (!normalized) continue;
    memberMap.set(normalized.name.toLowerCase(), normalized);
  }

  for (const rawMember of metaMembers) {
    const normalized = normalizeMemberRecord(rawMember);
    if (!normalized) continue;
    const key = normalized.name.toLowerCase();
    if (normalized.removedAt != null) {
      memberMap.delete(key);
      removedNames.add(key);
      continue;
    }
    removedNames.delete(key);
    memberMap.set(key, mergeResolvedMember(memberMap.get(key) || { name: normalized.name }, normalized));
  }

  for (const inboxName of inboxNames) {
    const normalized = String(inboxName || '').trim();
    if (!normalized) continue;
    const key = normalized.toLowerCase();
    if (!memberMap.has(key) && looksLikeQualifiedExternalRecipient(normalized)) continue;
    if (removedNames.has(key) || memberMap.has(key)) continue;
    memberMap.set(key, { name: normalized });
  }

  return {
    config,
    members: Array.from(memberMap.values()).sort((a, b) => a.name.localeCompare(b.name)),
    removedNames,
    inboxNames,
  };
}

function getCurrentRuntimeMemberIdentity() {
  const args = Array.isArray(process.argv) ? process.argv.slice(2) : [];
  let agentName = '';
  let agentId = '';
  let teamName = '';

  for (let i = 0; i < args.length; i += 1) {
    const arg = typeof args[i] === 'string' ? args[i] : '';
    const next = typeof args[i + 1] === 'string' ? args[i + 1].trim() : '';
    if (!next) continue;
    if (arg === '--agent-name') {
      agentName = next;
      continue;
    }
    if (arg === '--agent-id') {
      agentId = next;
      continue;
    }
    if (arg === '--team-name') {
      teamName = next;
    }
  }

  const normalizedAgentName = typeof agentName === 'string' ? agentName.trim() : '';
  const normalizedAgentId = typeof agentId === 'string' ? agentId.trim() : '';
  const normalizedTeamName = typeof teamName === 'string' ? teamName.trim() : '';
  if (!normalizedAgentName && !normalizedAgentId) {
    return null;
  }

  return {
    agentName: normalizedAgentName,
    agentId: normalizedAgentId,
    teamName: normalizedTeamName,
  };
}

function resolveLeadSessionId(paths) {
  const config = readTeamConfig(paths);
  return config && typeof config.leadSessionId === 'string' && config.leadSessionId.trim()
    ? config.leadSessionId.trim()
    : undefined;
}

function resolveCanonicalLeadSessionId(paths, candidate) {
  const configured = resolveLeadSessionId(paths);
  const explicit = typeof candidate === 'string' ? candidate.trim() : '';

  if (!explicit) {
    return configured;
  }

  // The team config is the canonical source of the current lead runtime session.
  // If a caller passes a placeholder like "team-lead" or any other mismatched value,
  // prefer the configured session id instead of persisting dirty metadata into inbox rows.
  if (configured) {
    return explicit === configured ? explicit : configured;
  }

  return explicit;
}

function isProcessAlive(pid) {
  try {
    process.kill(pid, 0);
    return true;
  } catch (error) {
    if (error && error.code === 'EPERM') {
      return true;
    }
    return false;
  }
}

function sanitizeFilename(original) {
  const raw = String(original == null ? '' : original).trim();
  const parts = raw.split(/[\\/]/);
  const base = (parts.length ? parts[parts.length - 1] : raw).trim();
  const cleaned = base
    .replace(/\0/g, '')
    .replace(/[\r\n\t]/g, ' ')
    .replace(/[\\/]/g, '_')
    .trim();
  if (!cleaned) return 'attachment';
  return cleaned.length > 180 ? cleaned.slice(0, 180) : cleaned;
}

function readFileHeader(filePath, maxBytes) {
  const fd = fs.openSync(filePath, 'r');
  try {
    const buffer = Buffer.alloc(maxBytes);
    const bytes = fs.readSync(fd, buffer, 0, maxBytes, 0);
    return buffer.slice(0, bytes);
  } finally {
    try {
      fs.closeSync(fd);
    } catch {
      // ignore
    }
  }
}

function startsWithBytes(buffer, bytes) {
  if (!buffer || buffer.length < bytes.length) return false;
  for (let i = 0; i < bytes.length; i += 1) {
    if (buffer[i] !== bytes[i]) return false;
  }
  return true;
}

function detectMimeTypeFromPathAndHeader(filePath, filename) {
  const name = String(filename || '').toLowerCase();
  const ext = path.extname(name);

  if (ext === '.png') return 'image/png';
  if (ext === '.jpg' || ext === '.jpeg') return 'image/jpeg';
  if (ext === '.gif') return 'image/gif';
  if (ext === '.webp') return 'image/webp';
  if (ext === '.pdf') return 'application/pdf';
  if (ext === '.txt') return 'text/plain';
  if (ext === '.md') return 'text/markdown';
  if (ext === '.json') return 'application/json';
  if (ext === '.zip') return 'application/zip';

  let header;
  try {
    header = readFileHeader(filePath, 16);
  } catch {
    return 'application/octet-stream';
  }

  if (startsWithBytes(header, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) return 'image/png';
  if (startsWithBytes(header, [0xff, 0xd8, 0xff])) return 'image/jpeg';
  if (header.length >= 6) {
    const signature6 = header.slice(0, 6).toString('ascii');
    if (signature6 === 'GIF87a' || signature6 === 'GIF89a') return 'image/gif';
  }
  if (header.length >= 12) {
    const riff = header.slice(0, 4).toString('ascii');
    const webp = header.slice(8, 12).toString('ascii');
    if (riff === 'RIFF' && webp === 'WEBP') return 'image/webp';
  }
  if (header.length >= 5 && header.slice(0, 5).toString('ascii') === '%PDF-') {
    return 'application/pdf';
  }
  if (startsWithBytes(header, [0x50, 0x4b, 0x03, 0x04])) return 'application/zip';

  return 'application/octet-stream';
}

function getTaskAttachmentsDir(paths, taskId) {
  const safeTaskId = assertSafePathSegment('taskId', taskId);
  return path.join(paths.teamDir, TASK_ATTACHMENTS_DIR, safeTaskId);
}

function getStoredAttachmentPath(paths, taskId, attachmentId, filename) {
  const safeFilename = sanitizeFilename(filename);
  return path.join(
    getTaskAttachmentsDir(paths, taskId),
    `${String(attachmentId)}--${safeFilename}`
  );
}

function ensureSourceFileReadable(srcPath) {
  const stats = fs.statSync(srcPath);
  if (!stats.isFile()) {
    throw new Error(`Not a file: ${String(srcPath)}`);
  }
  if (stats.size > MAX_TASK_ATTACHMENT_BYTES) {
    throw new Error(
      `Attachment too large: ${(stats.size / (1024 * 1024)).toFixed(1)} MB (max ${String(
        MAX_TASK_ATTACHMENT_BYTES / (1024 * 1024)
      )} MB)`
    );
  }
  return stats;
}

function copyOrLinkFile(srcPath, destPath, mode, allowFallback) {
  const normalizedMode = String(mode || 'copy').toLowerCase();
  if (normalizedMode === 'link') {
    try {
      fs.linkSync(srcPath, destPath);
      return { mode: 'link', fallbackUsed: false };
    } catch (error) {
      if (!allowFallback) throw error;
      try {
        fs.copyFileSync(srcPath, destPath);
        return { mode: 'copy', fallbackUsed: true };
      } catch (copyError) {
        throw copyError || error;
      }
    }
  }

  fs.copyFileSync(srcPath, destPath);
  return { mode: 'copy', fallbackUsed: false };
}

function saveTaskAttachmentFile(paths, taskId, flags) {
  const rawFile =
    (typeof flags.file === 'string' && flags.file.trim()) ||
    (typeof flags.path === 'string' && flags.path.trim()) ||
    '';
  if (!rawFile) {
    throw new Error('Missing --file <path>');
  }

  const srcPath = path.resolve(rawFile);
  ensureSourceFileReadable(srcPath);

  const filename =
    (typeof flags.filename === 'string' && flags.filename.trim()) || path.basename(srcPath);
  const mimeType =
    (typeof flags['mime-type'] === 'string' && flags['mime-type'].trim()) ||
    (typeof flags.mimeType === 'string' && flags.mimeType.trim()) ||
    detectMimeTypeFromPathAndHeader(srcPath, filename);

  const attachmentId = makeId();
  const dir = getTaskAttachmentsDir(paths, taskId);
  ensureDir(dir);
  const destPath = getStoredAttachmentPath(paths, taskId, attachmentId, filename);
  const allowFallback = !(flags['no-fallback'] === true);

  if (fs.existsSync(destPath)) {
    throw new Error('Attachment destination already exists');
  }

  const result = copyOrLinkFile(srcPath, destPath, flags.mode, allowFallback);
  const stats = fs.statSync(destPath);
  if (!stats.isFile() || stats.size < 0) {
    throw new Error('Attachment write verification failed');
  }

  const meta = {
    id: attachmentId,
    filename,
    mimeType,
    size: stats.size,
    addedAt: nowIso(),
    filePath: destPath,
  };

  return {
    meta,
    storedPath: destPath,
    storageMode: result.mode,
    fallbackUsed: result.fallbackUsed,
  };
}

module.exports = {
  assertExplicitTeamMemberName,
  collectExplicitTeamMembers,
  getPaths,
  inferLeadName,
  isCanonicalLeadMember,
  looksLikeCrossTeamRecipient,
  looksLikeCrossTeamToolRecipient,
  isProcessAlive,
  listInboxMemberNames,
  readMembersMeta,
  readTeamConfig,
  resolveExplicitTeamMemberName,
  resolveTaskCommentAuthorName,
  resolveTeamMembers,
  getCurrentRuntimeMemberIdentity,
  resolveCanonicalLeadSessionId,
  resolveLeadSessionId,
  saveTaskAttachmentFile,
};
