/**
 * Team Message Bus - JSONL-based persistent message log for Agent Teams
 *
 * Operations:
 * - log:       Append a message (to defaults to "coordinator", summary auto-generated if omitted)
 * - read:      Read message(s) by ID
 * - list:      List recent messages with optional filters (from/to/type/last N)
 * - status:    Summarize team member activity from message history
 * - delete:    Delete a specific message by ID
 * - clear:     Clear all messages for a team
 * - broadcast: Log a message with to="all"
 * - get_state: Read role state from meta.json
 */

import { z } from 'zod';
import type { ToolSchema, ToolResult } from '../types/tool.js';
import { existsSync, mkdirSync, readFileSync, appendFileSync, writeFileSync, rmSync, statSync } from 'fs';
import { join, dirname } from 'path';
import { getProjectRoot } from '../utils/path-validator.js';

// --- Team Metadata ---

export interface TeamMeta {
  status: 'active' | 'completed' | 'archived';
  created_at: string;
  updated_at: string;
  archived_at?: string;

  // Pipeline configuration (previously in team-session.json)
  pipeline_mode?: string;
  pipeline_stages?: string[];
  team_name?: string;
  task_description?: string;
  roles?: string[];

  // Role state snapshot (previously in shared-memory.json)
  role_state?: Record<string, Record<string, unknown>>;
}

export function getMetaPath(team: string): string {
  return join(getLogDir(team), 'meta.json');
}

export function readTeamMeta(team: string): TeamMeta | null {
  const metaPath = getMetaPath(team);
  if (!existsSync(metaPath)) return null;
  try {
    return JSON.parse(readFileSync(metaPath, 'utf-8')) as TeamMeta;
  } catch {
    return null;
  }
}

export function writeTeamMeta(team: string, meta: TeamMeta): void {
  const dir = getLogDir(team);
  if (!existsSync(dir)) {
    mkdirSync(dir, { recursive: true });
  }
  writeFileSync(getMetaPath(team), JSON.stringify(meta, null, 2), 'utf-8');
}

/**
 * Infer team status when no meta.json exists.
 * If last message is 'shutdown' → 'completed', otherwise 'active'.
 */
export function inferTeamStatus(team: string): TeamMeta['status'] {
  const messages = readAllMessages(team);
  if (messages.length === 0) return 'active';
  const lastMsg = messages[messages.length - 1];
  return lastMsg.type === 'shutdown' ? 'completed' : 'active';
}

/**
 * Get effective team meta: reads meta.json, with fallback to shared-memory.json
 * and team-session.json for backward compatibility with older sessions.
 */
export function getEffectiveTeamMeta(team: string): TeamMeta {
  const meta = readTeamMeta(team);
  if (meta) {
    // Enrich from legacy files if key fields are missing
    if (!meta.role_state || !meta.pipeline_mode || !meta.roles || !meta.pipeline_stages) {
      const legacyData = readLegacyFiles(team);
      if (!meta.pipeline_mode && legacyData.pipeline_mode) {
        meta.pipeline_mode = legacyData.pipeline_mode;
      }
      if (!meta.role_state && legacyData.role_state) {
        meta.role_state = legacyData.role_state;
      }
      if (!meta.pipeline_stages && legacyData.pipeline_stages) {
        meta.pipeline_stages = legacyData.pipeline_stages;
      }
      if (!meta.team_name && legacyData.team_name) {
        meta.team_name = legacyData.team_name;
      }
      if (!meta.roles && legacyData.roles) {
        meta.roles = legacyData.roles;
      }
    }
    return meta;
  }

  // No meta.json — build from legacy files + inferred status
  const status = inferTeamStatus(team);
  const dir = getLogDir(team);
  let created_at = new Date().toISOString();
  try {
    const stat = statSync(dir);
    created_at = stat.birthtime.toISOString();
  } catch { /* use now as fallback */ }

  const messages = readAllMessages(team);
  const lastMsg = messages[messages.length - 1];
  const updated_at = lastMsg?.ts || created_at;

  const legacyData = readLegacyFiles(team);

  return {
    status,
    created_at,
    updated_at,
    ...legacyData,
  };
}

/**
 * Read legacy files (shared-memory.json, team-session.json) for backward compatibility.
 */
function readLegacyFiles(team: string): Partial<TeamMeta> {
  const root = getProjectRoot();
  const sessionDir = join(root, '.workflow', '.team', team);
  const result: Partial<TeamMeta> = {};

  // Try shared-memory.json (role state + pipeline_mode)
  const sharedMemPath = join(sessionDir, 'shared-memory.json');
  if (existsSync(sharedMemPath)) {
    try {
      const sharedMem = JSON.parse(readFileSync(sharedMemPath, 'utf-8'));
      if (sharedMem.pipeline_mode) result.pipeline_mode = sharedMem.pipeline_mode;
      if (sharedMem.pipeline_stages) result.pipeline_stages = sharedMem.pipeline_stages;
      // Extract role state: any key that looks like a role namespace
      const roleState: Record<string, Record<string, unknown>> = {};
      for (const [key, value] of Object.entries(sharedMem)) {
        if (typeof value === 'object' && value !== null && !Array.isArray(value)
            && !['pipeline_mode', 'pipeline_stages'].includes(key)) {
          roleState[key] = value as Record<string, unknown>;
        }
      }
      if (Object.keys(roleState).length > 0) result.role_state = roleState;
    } catch { /* ignore parse errors */ }
  }

  // Try team-session.json (pipeline config)
  const sessionPath = join(sessionDir, 'team-session.json');
  if (existsSync(sessionPath)) {
    try {
      const session = JSON.parse(readFileSync(sessionPath, 'utf-8'));
      if (!result.pipeline_mode && session.pipeline_mode) result.pipeline_mode = session.pipeline_mode;
      if (!result.pipeline_stages && session.pipeline_stages) result.pipeline_stages = session.pipeline_stages;
      if (session.team_name) result.team_name = session.team_name;
      if (session.task_description) result.task_description = session.task_description;
      // Handle both string[] and { name: string }[] formats
      if (session.roles && Array.isArray(session.roles)) {
        if (typeof session.roles[0] === 'string') {
          result.roles = session.roles;
        } else if (typeof session.roles[0] === 'object' && session.roles[0] !== null && 'name' in session.roles[0]) {
          result.roles = session.roles.map((r: { name: string }) => r.name);
        }
      }
    } catch { /* ignore parse errors */ }
  }

  return result;
}

// --- Types ---

export interface TeamMessage {
  id: string;
  ts: string;
  from: string;
  to: string;
  type: string;
  summary: string;
  data?: Record<string, unknown>;
}

export interface StatusEntry {
  member: string;
  lastSeen: string;
  lastAction: string;
  messageCount: number;
}

// --- Zod Schema ---

const ParamsSchema = z.object({
  operation: z.enum(['log', 'read', 'list', 'status', 'delete', 'clear', 'broadcast', 'get_state']).describe('Operation to perform'),

  // Session identifier (primary)
  session_id: z.string().optional().describe('Session ID that determines message storage path (e.g., TLS-my-project-2026-02-27)'),

  // Legacy params (backward compat)
  team_session_id: z.string().optional().describe('[deprecated] Use session_id'),
  team: z.string().optional().describe('[deprecated] Use session_id'),

  // log/broadcast params
  from: z.string().optional().describe('[log/broadcast/list] Sender role name'),
  to: z.string().optional().describe('[log/list] Recipient role (defaults to "coordinator")'),
  type: z.string().optional().describe('[log/broadcast/list] Message type (state_update, plan_ready, shutdown, etc.)'),
  summary: z.string().optional().describe('[log/broadcast] One-line summary (auto-generated from type+from if omitted)'),
  data: z.record(z.string(), z.unknown()).optional().describe('[log/broadcast] Structured data payload. Use data.ref for file paths. When type="state_update", auto-synced to meta.json'),

  // read/delete params
  id: z.string().optional().describe('[read/delete] Message ID (e.g. MSG-003)'),

  // list params
  last: z.number().min(1).max(100).optional().describe('[list] Return last N messages (default: 20)'),

  // get_state params
  role: z.string().optional().describe('[get_state] Role name to query. Omit to get all role states'),

  // Legacy backward compat (accepted but ignored — session_id replaces this)
  ref: z.string().optional().describe('[deprecated] Use data.ref instead'),
});

type Params = z.infer<typeof ParamsSchema>;

/** Resolve team session ID from params, supporting legacy 'team_session_id' and 'team' */
function resolveTeamId(params: Params): string | null {
  return params.session_id || params.team_session_id || params.team || null;
}

// --- Tool Schema ---

export const schema: ToolSchema = {
  name: 'team_msg',
  description: `Team message bus - persistent JSONL log for Agent Team communication. Choose an operation and provide its required parameters.

**Storage Location:** .workflow/.team/{session-id}/.msg/messages.jsonl

**Operations & Required Parameters:**

*   **log**: Append a message to the log.
    *   **session_id** (string, **REQUIRED**): Session ID (e.g., TLS-my-project-2026-02-27).
    *   **from** (string, **REQUIRED**): Sender role name (e.g., "planner", "coordinator").
    *   *to* (string): Recipient role (default: "coordinator").
    *   *type* (string): Message type (default: "message").
    *   *summary* (string): One-line summary (auto-generated if omitted).
    *   *data* (object): Structured data payload. Use data.ref for file paths.

*   **broadcast**: Send message to all team members.
    *   **session_id** (string, **REQUIRED**): Session ID.
    *   **from** (string, **REQUIRED**): Sender role name.
    *   *type* (string): Message type (e.g., "shutdown").
    *   *summary* (string): One-line summary.
    *   *data* (object): Structured data payload.

*   **read**: Read a specific message by ID.
    *   **session_id** (string, **REQUIRED**): Session ID.
    *   **id** (string, **REQUIRED**): Message ID (e.g., "MSG-003").

*   **list**: List recent messages.
    *   **session_id** (string, **REQUIRED**): Session ID.
    *   *last* (number): Number of messages to show (default: 20, max: 100).
    *   *from* (string): Filter by sender role.
    *   *to* (string): Filter by recipient role.
    *   *type* (string): Filter by message type.

*   **status**: Summarize team member activity.
    *   **session_id** (string, **REQUIRED**): Session ID.

*   **get_state**: Get state for a specific role.
    *   **session_id** (string, **REQUIRED**): Session ID.
    *   *role* (string): Role name to query (omit for all roles).

*   **delete**: Delete a message by ID.
    *   **session_id** (string, **REQUIRED**): Session ID.
    *   **id** (string, **REQUIRED**): Message ID to delete.

*   **clear**: Clear all messages for a session.
    *   **session_id** (string, **REQUIRED**): Session ID.

**Common Message Types:** plan_ready, plan_approved, plan_revision, task_unblocked, impl_complete, impl_progress, test_result, review_result, fix_required, error, shutdown, state_update

**Session ID Format:** TLS-{name}-{date} (e.g., TLS-my-project-2026-02-27)`,
  inputSchema: {
    type: 'object',
    properties: {
      operation: {
        type: 'string',
        enum: ['log', 'read', 'list', 'status', 'delete', 'clear', 'broadcast', 'get_state'],
        description: 'Operation: log | read | list | status | delete | clear | broadcast | get_state',
      },
      session_id: {
        type: 'string',
        description: 'Session ID (e.g., TLS-my-project-2026-02-27). Maps to .workflow/.team/{session-id}/.msg/',
      },
      from: { type: 'string', description: '[log/broadcast/list] Sender role' },
      to: { type: 'string', description: '[log/list] Recipient role (defaults to "coordinator")' },
      type: { type: 'string', description: '[log/broadcast/list] Message type' },
      summary: { type: 'string', description: '[log/broadcast] One-line summary (auto-generated if omitted)' },
      data: { type: 'object', description: '[log/broadcast] Structured data. Use data.ref for file paths. When type="state_update", auto-synced to meta.json' },
      id: { type: 'string', description: '[read/delete] Message ID (e.g. MSG-003)' },
      last: { type: 'number', description: '[list] Last N messages (default 20)', minimum: 1, maximum: 100 },
      role: { type: 'string', description: '[get_state] Role name to query. Omit for all roles' },
      // Legacy params (backward compat)
      team_session_id: { type: 'string', description: '[deprecated] Use session_id' },
      team: { type: 'string', description: '[deprecated] Use session_id' },
      ref: { type: 'string', description: '[deprecated] Use data.ref instead' },
    },
    required: ['operation'],
  },
};

// --- Helpers ---

/**
 * Get the log directory for a session.
 * New structure: .workflow/.team/{session-id}/.msg/
 */
export function getLogDir(sessionId: string): string {
  const root = getProjectRoot();
  return join(root, '.workflow', '.team', sessionId, '.msg');
}

/**
 * Legacy support: Check both new (.team/{id}/.msg) and old (.team-msg/{id}) locations
 */
export function getLogDirWithFallback(sessionId: string): string {
  const newPath = getLogDir(sessionId);
  if (existsSync(newPath)) {
    return newPath;
  }
  // Fallback to old location for backward compatibility
  const root = getProjectRoot();
  return join(root, '.workflow', '.team-msg', sessionId);
}

function getLogPath(teamId: string): string {
  return join(getLogDir(teamId), 'messages.jsonl');
}

function ensureLogFile(teamId: string): string {
  const logPath = getLogPath(teamId);
  const dir = dirname(logPath);
  if (!existsSync(dir)) {
    mkdirSync(dir, { recursive: true });
  }
  if (!existsSync(logPath)) {
    appendFileSync(logPath, '', 'utf-8');
  }
  return logPath;
}

export function readAllMessages(teamId: string): TeamMessage[] {
  const logPath = getLogPath(teamId);
  if (!existsSync(logPath)) return [];

  const content = readFileSync(logPath, 'utf-8').trim();
  if (!content) return [];

  return content.split('\n').map(line => {
    try {
      return JSON.parse(line) as TeamMessage;
    } catch {
      return null;
    }
  }).filter((m): m is TeamMessage => m !== null);
}

function getNextId(messages: TeamMessage[]): string {
  const maxNum = messages.reduce((max, m) => {
    const match = m.id.match(/^MSG-(\d+)$/);
    return match ? Math.max(max, parseInt(match[1], 10)) : max;
  }, 0);
  return `MSG-${String(maxNum + 1).padStart(3, '0')}`;
}

function nowISO(): string {
  return new Date().toISOString().replace('Z', '+00:00');
}

// --- Operations ---

function opLog(params: Params, teamId: string): ToolResult {
  if (!params.from) return { success: false, error: 'log requires "from"' };

  // Default to "coordinator" if to is not specified
  const to = params.to || 'coordinator';

  // Backward compat: merge legacy ref into data.ref
  if (params.ref) {
    if (!params.data) params.data = {};
    if (!params.data.ref) params.data.ref = params.ref;
  }

  // Auto-generate summary if not provided
  const summary = params.summary || `[${params.from}] ${params.type || 'message'} → ${to}`;

  const logPath = ensureLogFile(teamId);
  const messages = readAllMessages(teamId);
  const id = getNextId(messages);

  const msg: TeamMessage = {
    id,
    ts: nowISO(),
    from: params.from,
    to,
    type: params.type || 'message',
    summary,
  };
  if (params.data) msg.data = params.data;

  appendFileSync(logPath, JSON.stringify(msg) + '\n', 'utf-8');

  // Auto-sync state_update to meta.json
  if (params.type === 'state_update' && params.data) {
    const meta = getEffectiveTeamMeta(teamId);

    // Role state: store under role_state namespace
    if (params.from) {
      if (!meta.role_state) meta.role_state = {};
      meta.role_state[params.from] = {
        ...meta.role_state[params.from],
        ...params.data,
        _updated_at: nowISO(),
      };
    }

    // Promote top-level keys directly to meta
    const topLevelKeys = ['pipeline_mode', 'pipeline_stages', 'team_name', 'task_description', 'roles'] as const;
    for (const key of topLevelKeys) {
      if (params.data[key] !== undefined) {
        (meta as any)[key] = params.data[key];
      }
    }

    meta.updated_at = nowISO();
    writeTeamMeta(teamId, meta);
  }

  return { success: true, result: { id, message: `Logged ${id}: [${msg.from} → ${msg.to}] ${msg.summary}` } };
}

function opRead(params: Params, teamId: string): ToolResult {
  if (!params.id) return { success: false, error: 'read requires "id"' };

  const messages = readAllMessages(teamId);
  const msg = messages.find(m => m.id === params.id);

  if (!msg) {
    return { success: false, error: `Message ${params.id} not found in team "${teamId}"` };
  }

  return { success: true, result: msg };
}

function opList(params: Params, teamId: string): ToolResult {
  let messages = readAllMessages(teamId);

  // Apply filters
  if (params.from) messages = messages.filter(m => m.from === params.from);
  if (params.to) messages = messages.filter(m => m.to === params.to);
  if (params.type) messages = messages.filter(m => m.type === params.type);

  // Take last N
  const last = params.last || 20;
  const sliced = messages.slice(-last);

  const lines = sliced.map(m => `${m.id} [${m.ts.substring(11, 19)}] ${m.from} → ${m.to} (${m.type}) ${m.summary}`);

  return {
    success: true,
    result: {
      total: messages.length,
      showing: sliced.length,
      messages: sliced,
      formatted: lines.join('\n'),
    },
  };
}

function opStatus(params: Params, teamId: string): ToolResult {
  const messages = readAllMessages(teamId);

  if (messages.length === 0) {
    return { success: true, result: { members: [], summary: 'No messages recorded yet.' } };
  }

  // Aggregate per-member stats
  const memberMap = new Map<string, StatusEntry>();

  for (const msg of messages) {
    for (const role of [msg.from, msg.to]) {
      if (!memberMap.has(role)) {
        memberMap.set(role, { member: role, lastSeen: msg.ts, lastAction: '', messageCount: 0 });
      }
    }
    const fromEntry = memberMap.get(msg.from)!;
    fromEntry.lastSeen = msg.ts;
    fromEntry.lastAction = `sent ${msg.type} → ${msg.to}`;
    fromEntry.messageCount++;
  }

  const members = Array.from(memberMap.values()).sort((a, b) => b.lastSeen.localeCompare(a.lastSeen));

  const formatted = members.map(m =>
    `${m.member.padEnd(12)} | last: ${m.lastSeen.substring(11, 19)} | msgs: ${m.messageCount} | ${m.lastAction}`
  ).join('\n');

  return {
    success: true,
    result: {
      members,
      total_messages: messages.length,
      formatted,
    },
  };
}

function opDelete(params: Params, teamId: string): ToolResult {
  if (!params.id) return { success: false, error: 'delete requires "id"' };

  const messages = readAllMessages(teamId);
  const idx = messages.findIndex(m => m.id === params.id);

  if (idx === -1) {
    return { success: false, error: `Message ${params.id} not found in team "${teamId}"` };
  }

  const removed = messages.splice(idx, 1)[0];
  const logPath = ensureLogFile(teamId);
  writeFileSync(logPath, messages.map(m => JSON.stringify(m)).join('\n') + (messages.length > 0 ? '\n' : ''), 'utf-8');

  return { success: true, result: { deleted: removed.id, message: `Deleted ${removed.id}: [${removed.from} → ${removed.to}] ${removed.summary}` } };
}

function opBroadcast(params: Params, teamId: string): ToolResult {
  if (!params.from) return { success: false, error: 'broadcast requires "from"' };

  // Delegate to opLog with to="all"
  return opLog({ ...params, operation: 'log', to: 'all' }, teamId);
}

function opGetState(params: Params, teamId: string): ToolResult {
  const meta = getEffectiveTeamMeta(teamId);
  const roleState = meta.role_state || {};

  if (params.role) {
    const state = roleState[params.role];
    if (!state) {
      return { success: true, result: { role: params.role, state: null, message: `No state found for role "${params.role}"` } };
    }
    return { success: true, result: { role: params.role, state } };
  }

  return { success: true, result: { role_state: roleState } };
}

function opClear(params: Params, teamId: string): ToolResult {
  const logPath = getLogPath(teamId);
  const dir = getLogDir(teamId);

  if (!existsSync(logPath)) {
    return { success: true, result: { message: `Team "${teamId}" has no messages to clear.` } };
  }

  const count = readAllMessages(teamId).length;
  rmSync(dir, { recursive: true, force: true });

  return { success: true, result: { cleared: count, message: `Cleared ${count} messages for team "${teamId}".` } };
}

// --- Handler ---

export async function handler(params: Record<string, unknown>): Promise<ToolResult> {
  const parsed = ParamsSchema.safeParse(params);
  if (!parsed.success) {
    return { success: false, error: `Invalid params: ${parsed.error.message}` };
  }

  const p = parsed.data;

  // Resolve team ID from session_id / team_session_id / team (backward compat)
  const teamId = resolveTeamId(p);
  if (!teamId) {
    return { success: false, error: 'Missing required parameter: session_id (or legacy "team_session_id" / "team")' };
  }

  switch (p.operation) {
    case 'log': return opLog(p, teamId);
    case 'read': return opRead(p, teamId);
    case 'list': return opList(p, teamId);
    case 'status': return opStatus(p, teamId);
    case 'delete': return opDelete(p, teamId);
    case 'clear': return opClear(p, teamId);
    case 'broadcast': return opBroadcast(p, teamId);
    case 'get_state': return opGetState(p, teamId);
    default:
      return { success: false, error: `Unknown operation: ${p.operation}` };
  }
}
