import {
  readdirSync,
  readFileSync,
  existsSync,
  statSync,
  mkdirSync,
  rmSync,
  appendFileSync,
  openSync,
  readSync,
  closeSync,
} from 'node:fs';
import { join } from 'node:path';
import { DATA_PATH } from '../../data-path';

// ── Claude session store (source of truth) ──
// The `claude` CLI persists every session as a JSONL transcript at
//   $HOME/.claude/projects/<slug>/<session-uuid>.jsonl
// where <slug> is the working directory with every non-alphanumeric char replaced by '-'.
// Single-user platform: Claude runs with no isolation — HOME is the real home
// (HOME_DIR) — so its transcripts are the same store the terminal `claude` uses. We never keep our
// own copy; Claude's files are authoritative.

const claudeHome = (email: string): string => process.env.HOME_DIR ?? join(DATA_PATH, email, 'home');

/** Dedicated working directory for /chat sessions, so they form their own Claude "project" group. */
export const getGeneralChatSessionsCwd = (email: string): string => join(DATA_PATH, email, 'general_chat_sessions');

/** Same, but create the directory if it doesn't exist (call before spawning a /chat session). */
export const ensureGeneralChatSessionsCwd = (email: string): string => {
  const dir = getGeneralChatSessionsCwd(email);
  mkdirSync(dir, { recursive: true });
  return dir;
};

const claudeProjectsDir = (email: string): string => join(claudeHome(email), '.claude', 'projects');

/** Claude's folder name for a working directory. */
export const projectSlug = (cwd: string): string => cwd.replace(/[^a-zA-Z0-9]/g, '-');

export type ClaudeSessionSummary = {
  id: string; // session uuid (= transcript filename)
  title: string; // first human message, trimmed
  cwd: string;
  createdAt: string; // ISO, first entry
  updatedAt: string; // ISO, last entry
  messageCount: number; // human + assistant turns
  harness?: 'claude' | 'opencode'; // which harness produced it (defaults to claude when absent)
};

/** Pull the plain text out of a transcript entry's `message.content` (string or content-block array). */
function entryText(message: unknown): string {
  if (!message || typeof message !== 'object') return '';
  const content = (message as { content?: unknown }).content;
  if (typeof content === 'string') return content;
  if (Array.isArray(content)) {
    return content
      .map((block) =>
        block && typeof block === 'object' && (block as { type?: string }).type === 'text'
          ? ((block as { text?: string }).text ?? '')
          : '',
      )
      .join('')
      .trim();
  }
  return '';
}

type Entry = {
  type?: string;
  timestamp?: string;
  sessionId?: string;
  cwd?: string;
  uuid?: string;
  summary?: string;
  message?: { role?: string; content?: unknown };
  isMeta?: boolean;
};

function summarizeTranscript(filePath: string, id: string): ClaudeSessionSummary | null {
  let raw: string;
  try {
    raw = readFileSync(filePath, 'utf-8');
  } catch {
    return null;
  }

  let firstUserText = '';
  let summaryTitle = ''; // a `summary` entry (our rename, appended to the transcript) wins over the first message
  let cwd = '';
  let firstTs = '';
  let lastTs = '';
  let messageCount = 0;

  for (const line of raw.split('\n')) {
    if (!line.trim()) continue;
    let entry: Entry;
    try {
      entry = JSON.parse(line) as Entry;
    } catch {
      continue;
    }
    if (entry.cwd && !cwd) cwd = entry.cwd;
    if (entry.timestamp) {
      if (!firstTs) firstTs = entry.timestamp;
      lastTs = entry.timestamp;
    }
    if (entry.type === 'summary' && typeof entry.summary === 'string') {
      summaryTitle = entry.summary; // last one wins
    } else if (entry.type === 'user' || entry.type === 'assistant') {
      messageCount += 1;
      if (!firstUserText && entry.type === 'user' && !entry.isMeta) {
        firstUserText = entryText(entry.message).split('\n')[0]!.trim();
      }
    }
  }

  const mtime = statSync(filePath).mtime.toISOString();
  return {
    id,
    title: summaryTitle || firstUserText || '(untitled)',
    cwd,
    createdAt: firstTs || mtime,
    updatedAt: lastTs || mtime,
    messageCount,
  };
}

// ── Loading a full transcript for display ──
// App-facing message shape (matches the frontend ChatMessage union), rebuilt from Claude's blocks.
export type ClaudeChatMessage =
  | { role: 'user'; text: string }
  | { role: 'assistant'; id: string; text: string }
  | {
      role: 'tool';
      toolName: string;
      toolInput: Record<string, unknown>;
      toolCallId: string;
      output?: string;
      isError?: boolean;
    };

type ContentBlock =
  | { type: 'text'; text?: string }
  | { type: 'thinking' }
  | { type: 'tool_use'; id: string; name: string; input?: Record<string, unknown> }
  | { type: 'tool_result'; tool_use_id: string; is_error?: boolean | null; content?: unknown };

function blockText(content: unknown): string {
  if (typeof content === 'string') return content;
  if (Array.isArray(content)) {
    return content
      .map((b) =>
        b && typeof b === 'object' && (b as { type?: string }).type === 'text'
          ? ((b as { text?: string }).text ?? '')
          : '',
      )
      .join('');
  }
  return '';
}

export type ClaudeSessionDetail = { id: string; model: string; cwd: string; messages: ClaudeChatMessage[] };

/** Parse a session's JSONL transcript into a flat, display-ready message list. Claude is the source. */
export function loadClaudeSession(email: string, cwd: string, sessionId: string): ClaudeSessionDetail | null {
  const filePath = join(claudeProjectsDir(email), projectSlug(cwd), `${sessionId}.jsonl`);
  if (!existsSync(filePath)) return null;

  const messages: ClaudeChatMessage[] = [];
  const toolById = new Map<string, Extract<ClaudeChatMessage, { role: 'tool' }>>();
  let model = '';
  let sessionCwd = cwd;

  for (const line of readFileSync(filePath, 'utf-8').split('\n')) {
    if (!line.trim()) continue;
    let entry: Entry & { message?: { role?: string; content?: unknown; model?: string } };
    try {
      entry = JSON.parse(line);
    } catch {
      continue;
    }
    if (entry.cwd) sessionCwd = entry.cwd;
    if (entry.message?.model && !model) model = entry.message.model;

    const content = entry.message?.content;

    if (entry.type === 'user' && !entry.isMeta) {
      if (typeof content === 'string') {
        if (content.trim()) messages.push({ role: 'user', text: content });
        continue;
      }
      if (Array.isArray(content)) {
        for (const block of content as ContentBlock[]) {
          if (block.type === 'text' && block.text?.trim()) {
            messages.push({ role: 'user', text: block.text });
          } else if (block.type === 'tool_result') {
            const tool = toolById.get(block.tool_use_id);
            if (tool) {
              tool.output = blockText(block.content);
              tool.isError = block.is_error === true;
            }
          }
        }
      }
      continue;
    }

    if (entry.type === 'assistant' && Array.isArray(content)) {
      for (const block of content as ContentBlock[]) {
        if (block.type === 'text' && block.text?.trim()) {
          messages.push({ role: 'assistant', id: `${sessionId}-${messages.length}`, text: block.text });
        } else if (block.type === 'tool_use') {
          const tool = {
            role: 'tool' as const,
            toolName: block.name,
            toolInput: block.input ?? {},
            toolCallId: block.id,
          };
          messages.push(tool);
          toolById.set(block.id, tool);
        }
      }
    }
  }

  return { id: sessionId, model, cwd: sessionCwd, messages };
}

/** Delete a session by removing its transcript file. Returns false if it didn't exist. */
export function deleteClaudeSession(email: string, cwd: string, sessionId: string): boolean {
  const filePath = join(claudeProjectsDir(email), projectSlug(cwd), `${sessionId}.jsonl`);
  if (!existsSync(filePath)) return false;
  rmSync(filePath);
  return true;
}

/**
 * Rename a session by appending a `summary` entry to its transcript — Claude's own on-disk format, so
 * the title lives in .claude (source of truth). Our reader takes the last summary as the title; no
 * timestamp is written so the rename doesn't reorder the list.
 */
export function renameClaudeSession(email: string, cwd: string, sessionId: string, title: string): boolean {
  const filePath = join(claudeProjectsDir(email), projectSlug(cwd), `${sessionId}.jsonl`);
  if (!existsSync(filePath)) return false;

  // Attach the summary to the transcript's tip (the last entry carrying a uuid).
  let leafUuid = sessionId;
  const lines = readFileSync(filePath, 'utf-8').split('\n');
  for (let i = lines.length - 1; i >= 0; i--) {
    if (!lines[i]!.trim()) continue;
    try {
      const entry = JSON.parse(lines[i]!) as Entry;
      if (entry.uuid) {
        leafUuid = entry.uuid;
        break;
      }
    } catch {
      /* skip */
    }
  }

  appendFileSync(filePath, `${JSON.stringify({ type: 'summary', summary: title, leafUuid })}\n`);
  return true;
}

// ── Working directories (pwds) ──
// Every dir you've run Claude in shows up as a project group under ~/.claude/projects. We read the
// real `cwd` back from each group's transcripts so the UI can offer "jump to any project's sessions".

/** Read the `cwd` recorded in a transcript, from a bounded head read (cwd appears in early entries). */
function firstCwd(filePath: string): string {
  let fd: number | undefined;
  try {
    fd = openSync(filePath, 'r');
    const buf = Buffer.alloc(32768);
    const n = readSync(fd, buf, 0, buf.length, 0);
    return buf.toString('utf-8', 0, n).match(/"cwd":"([^"]*)"/)?.[1] ?? '';
  } catch {
    return '';
  } finally {
    if (fd !== undefined) closeSync(fd);
  }
}

export type ClaudePwd = { cwd: string; sessionCount: number; updatedAt: string; isDefault: boolean };

/** All working directories that have Claude sessions, plus the default /chat dir. Newest first. */
export function listClaudePwds(email: string): ClaudePwd[] {
  const projectsDir = claudeProjectsDir(email);
  const defaultCwd = getGeneralChatSessionsCwd(email);
  const byCwd = new Map<string, { count: number; updatedAt: string }>();

  if (existsSync(projectsDir)) {
    for (const group of readdirSync(projectsDir)) {
      const groupDir = join(projectsDir, group);
      let files: string[];
      try {
        files = readdirSync(groupDir).filter((f) => f.endsWith('.jsonl'));
      } catch {
        continue; // not a directory
      }
      if (files.length === 0) continue;

      const cwd = firstCwd(join(groupDir, files[0]!));
      if (!cwd) continue;
      let updatedAt = '';
      for (const f of files) {
        const m = statSync(join(groupDir, f)).mtime.toISOString();
        if (m > updatedAt) updatedAt = m;
      }
      const prev = byCwd.get(cwd);
      byCwd.set(cwd, {
        count: (prev?.count ?? 0) + files.length,
        updatedAt: prev && prev.updatedAt > updatedAt ? prev.updatedAt : updatedAt,
      });
    }
  }

  if (!byCwd.has(defaultCwd)) byCwd.set(defaultCwd, { count: 0, updatedAt: '' });

  return [...byCwd.entries()]
    .map(([cwd, v]) => ({ cwd, sessionCount: v.count, updatedAt: v.updatedAt, isDefault: cwd === defaultCwd }))
    .sort((a, b) => (a.isDefault ? -1 : b.isDefault ? 1 : b.updatedAt.localeCompare(a.updatedAt)));
}

/** List sessions Claude has stored for a given working directory, newest first. */
export function listClaudeSessions(email: string, cwd: string): ClaudeSessionSummary[] {
  const dir = join(claudeProjectsDir(email), projectSlug(cwd));
  if (!existsSync(dir)) return [];

  const sessions: ClaudeSessionSummary[] = [];
  for (const file of readdirSync(dir)) {
    if (!file.endsWith('.jsonl')) continue;
    const summary = summarizeTranscript(join(dir, file), file.replace(/\.jsonl$/, ''));
    if (summary) sessions.push(summary);
  }
  return sessions.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
}
