import { mkdirSync, writeFileSync, renameSync, unlinkSync, realpathSync, cpSync, rmSync, existsSync, readdirSync, lstatSync } from 'fs';
import { join } from 'path';
import type { ToolDef } from '../../llm/openai-compat.js';
import type { ToolContext, ToolResult } from './core.js';
import { VALID_SKILL_NAME } from '../skills.js';
import { scanSkillContent, scanSkillDirectory, maxSeverity } from '../skills-scanner.js';
import { logger } from '../../logger.js';

// ── Injected deps (server.ts / worker.ts call setSkillToolDeps) ─────────────

export interface SkillToolDeps {
  auditLog?: (action: string, detail: object, jobId?: string | null) => void;
  userFolderRoot: string;
}

let _deps: SkillToolDeps | null = null;

export function setSkillToolDeps(deps: SkillToolDeps | null): void {
  _deps = deps;
}

// ── Skill materialization ───────────────────────────────────────────────────
// When the agent ReadSkill's a directory-based skill, copy its files into the
// task workspace (`{workspace}/skills/{name}/`) so its scripts are usable in
// every Bash sandbox mode (the skill store lives outside the workspace and is
// only bind-mounted in the bwrap path). The copy is rw and idempotent per task.

const SKILL_MATERIALIZE_MAX_BYTES = 50 * 1024 * 1024; // 50MB

/** Total size of `dir` (skipping symlinks), or null once it exceeds `cap`. */
function dirSizeCapped(dir: string, cap: number): number | null {
  let total = 0;
  const stack = [dir];
  while (stack.length > 0) {
    const d = stack.pop()!;
    let entries: string[];
    try { entries = readdirSync(d); } catch { continue; }
    for (const e of entries) {
      const p = join(d, e);
      let st;
      try { st = lstatSync(p); } catch { continue; }
      if (st.isSymbolicLink()) continue;
      if (st.isDirectory()) { stack.push(p); continue; }
      total += st.size;
      if (total > cap) return null;
    }
  }
  return total;
}

interface MaterializeResult { ok: boolean; relPath: string; note?: string }

/** Copy a skill's source dir into `{workspace}/skills/{name}/` (idempotent). */
export function materializeSkill(srcDir: string, workspacePath: string, skillName: string): MaterializeResult {
  const relPath = `skills/${skillName}`;
  const dest = join(workspacePath, 'skills', skillName);
  if (existsSync(dest)) return { ok: true, relPath }; // already materialized this task
  if (dirSizeCapped(srcDir, SKILL_MATERIALIZE_MAX_BYTES) === null) {
    return { ok: false, relPath, note: 'skill exceeds 50MB copy limit' };
  }
  try {
    mkdirSync(join(workspacePath, 'skills'), { recursive: true });
    cpSync(srcDir, dest, {
      recursive: true,
      dereference: false,
      // Skip symlinks so a link inside the skill cannot point outside the workspace.
      filter: (src) => { try { return !lstatSync(src).isSymbolicLink(); } catch { return false; } },
    });
    return { ok: true, relPath };
  } catch (e) {
    return { ok: false, relPath, note: `copy failed: ${(e as Error).message}` };
  }
}

// ---------------------------------------------------------------------------
// Tool definitions
// ---------------------------------------------------------------------------
export const TOOL_DEFS: Record<string, ToolDef> = {
  InstallSkill: {
    type: 'function',
    function: {
      name: 'InstallSkill',
      description: 'スキル（参照知識: 手順書・ガイド）をインストール。Piece（実行テンプレート）とは異なる。通常は content に SKILL.md 全文を渡す。workspace 内にスキルディレクトリを構築済みの場合のみ sourcePath を使用。',
      parameters: {
        type: 'object',
        properties: {
          name: { type: 'string', description: 'スキル名 ([a-z0-9_-] のみ)' },
          content: { type: 'string', description: 'SKILL.md の全文 (YAML frontmatter + 本文)。通常はこちらを使用' },
          sourcePath: { type: 'string', description: 'workspace 内のスキルディレクトリの絶対パス (SKILL.md + scripts/ 等を含む場合のみ)。workspace 外のパスは拒否される' },
          scope: { type: 'string', enum: ['system', 'user'], description: 'system=全ユーザー共有 (admin only), user=個人' },
        },
        required: ['name', 'scope'],
      },
    },
  },
  ReadSkill: {
    type: 'function',
    function: {
      name: 'ReadSkill',
      description: 'スキル（参照知識: 手順書・ガイド・規約）の全文を取得する。Piece の定義取得には GetPiece を使うこと。利用可能なスキル一覧はシステムプロンプトの Skills Index を参照。',
      parameters: {
        type: 'object',
        properties: {
          name: { type: 'string', description: 'スキル名 (Skills Index に表示されている名前)' },
        },
        required: ['name'],
      },
    },
  },
  ListSkills: {
    type: 'function',
    function: {
      name: 'ListSkills',
      description: 'インストール済みスキル（参照知識）の一覧を返す。Piece（実行テンプレート）の一覧は ListPieces を使うこと。',
      parameters: {
        type: 'object',
        properties: {},
        required: [],
      },
    },
  },
};

// ── InstallSkill implementation ──────────────────────────────────────────────

const MAX_FILE_COUNT = 100;
const MAX_DEPTH = 3;
const MAX_TOTAL_BYTES = 5 * 1024 * 1024; // 5MB

function executeInstallSkill(
  input: Record<string, unknown>,
  ctx: ToolContext,
): ToolResult {
  const skillName = input['name'] as string | undefined;
  const content = input['content'] as string | undefined;
  const sourcePath = input['sourcePath'] as string | undefined;
  const scope = input['scope'] as string | undefined;

  if (!skillName || typeof skillName !== 'string') {
    return { output: 'InstallSkill: "name" parameter is required', isError: true };
  }
  if (!content && !sourcePath) {
    return { output: 'InstallSkill: either "content" or "sourcePath" is required', isError: true };
  }
  if (content && sourcePath) {
    return { output: 'InstallSkill: specify either "content" or "sourcePath", not both', isError: true };
  }
  if (scope !== 'system' && scope !== 'user') {
    return { output: 'InstallSkill: "scope" must be "system" or "user"', isError: true };
  }
  if (!VALID_SKILL_NAME.test(skillName)) {
    return { output: `InstallSkill: invalid skill name "${skillName}". Only [a-z0-9_-] allowed.`, isError: true };
  }
  if (scope === 'system' && ctx.notesUserRole !== 'admin') {
    return { output: 'InstallSkill: system-scope install requires admin role', isError: true };
  }

  const catalog = ctx.skillCatalog;
  if (!catalog) {
    return { output: 'InstallSkill: skill catalog not available', isError: true };
  }
  const userId = ctx.userId ?? 'local';

  // Count check (user scope only)
  if (scope === 'user') {
    const userSkillCount = catalog.getForUser(userId).filter(s => s.source === 'user').length;
    if (userSkillCount >= 50) {
      return { output: `InstallSkill: user skill limit reached (${userSkillCount}/50)`, isError: true };
    }
  }

  // --- sourcePath mode: copy directory from workspace ---
  if (sourcePath) {
    let realSource: string;
    let realWorkspace: string;
    try {
      realSource = realpathSync(sourcePath);
      realWorkspace = realpathSync(ctx.workspacePath);
    } catch (e) {
      return { output: `InstallSkill: sourcePath does not exist or is not accessible. Use "content" parameter instead to pass SKILL.md text directly.`, isError: true };
    }
    if (!realSource.startsWith(realWorkspace + '/')) {
      return { output: 'InstallSkill: sourcePath must be inside the task workspace. Use "content" parameter to pass SKILL.md text directly.', isError: true };
    }
    if (!existsSync(join(realSource, 'SKILL.md'))) {
      return { output: `InstallSkill: SKILL.md not found in ${sourcePath}`, isError: true };
    }

    const stats = getDirStats(realSource);
    if (stats.fileCount > MAX_FILE_COUNT) {
      return { output: `InstallSkill: directory contains ${stats.fileCount} files (max ${MAX_FILE_COUNT})`, isError: true };
    }
    if (stats.maxDepth > MAX_DEPTH) {
      return { output: `InstallSkill: directory depth ${stats.maxDepth} exceeds max ${MAX_DEPTH}`, isError: true };
    }
    if (stats.totalBytes > MAX_TOTAL_BYTES) {
      return { output: `InstallSkill: directory size ${(stats.totalBytes / 1024 / 1024).toFixed(1)}MB exceeds max 5MB`, isError: true };
    }

    const findings = scanSkillDirectory(realSource);
    const severity = maxSeverity(findings);
    if (severity === 'high') {
      const details = findings.filter(f => f.severity === 'high').slice(0, 5)
        .map(f => `  - [${f.pattern}] ${f.match} (line ${f.line})`).join('\n');
      return { output: `InstallSkill: blocked by security scan:\n${details}`, isError: true };
    }

    const targetBase = scope === 'system' ? catalog.getSystemDir() : catalog.getUserSkillDir(userId);
    const target = join(targetBase, skillName);
    const tmpTarget = target + '.tmp-' + Date.now();

    try {
      mkdirSync(targetBase, { recursive: true });
      cpSync(realSource, tmpTarget, { recursive: true });
      if (existsSync(target)) rmSync(target, { recursive: true, force: true });
      const flatPath = join(targetBase, `${skillName}.md`);
      if (existsSync(flatPath)) unlinkSync(flatPath);
      renameSync(tmpTarget, target);
    } catch (e) {
      try { rmSync(tmpTarget, { recursive: true, force: true }); } catch {}
      return { output: `InstallSkill: install failed: ${e}`, isError: true };
    }

    if (scope === 'system') { catalog.refreshSystem(); } else { catalog.invalidate(userId); }
    _deps?.auditLog?.('skill_installed', { skillName, scope, userId, format: 'directory', fileCount: stats.fileCount }, ctx.taskId ?? null);

    const mediumFindings = findings.filter(f => f.severity === 'medium');
    let msg = `InstallSkill: installed "${skillName}" to ${scope} scope (${stats.fileCount} files)`;
    if (mediumFindings.length > 0) {
      msg += `\n\nWarnings: ${mediumFindings.length} medium-severity findings`;
    }
    return { output: msg, isError: false };
  }

  // --- content mode: create {name}/SKILL.md ---
  const MAX_BYTES = 64 * 1024;
  if (Buffer.byteLength(content!, 'utf-8') > MAX_BYTES) {
    return { output: `InstallSkill: content exceeds 64 KB limit`, isError: true };
  }

  const findings = scanSkillContent(content!);
  const severity = maxSeverity(findings);
  if (severity === 'high') {
    const details = findings.filter(f => f.severity === 'high')
      .map(f => `  - [high] ${f.pattern}: "${f.match}" (line ${f.line})`).join('\n');
    return { output: `InstallSkill: blocked by security scan:\n${details}`, isError: true };
  }

  const targetDir = scope === 'system' ? catalog.getSystemDir() : catalog.getUserSkillDir(userId);
  const skillDir = join(targetDir, skillName);
  const tmpDir = join(targetDir, `.${skillName}.tmp.${Date.now()}`);

  try {
    mkdirSync(tmpDir, { recursive: true });
    writeFileSync(join(tmpDir, 'SKILL.md'), content!, { encoding: 'utf-8', mode: 0o600 });
    if (existsSync(skillDir)) rmSync(skillDir, { recursive: true, force: true });
    const flatPath = join(targetDir, `${skillName}.md`);
    if (existsSync(flatPath)) unlinkSync(flatPath);
    renameSync(tmpDir, skillDir);
  } catch (err) {
    try { rmSync(tmpDir, { recursive: true, force: true }); } catch {}
    return { output: `InstallSkill: write failed: ${(err as Error).message}`, isError: true };
  }

  if (scope === 'system') { catalog.refreshSystem(); } else { catalog.invalidate(userId); }
  _deps?.auditLog?.('skill_installed', { skillName, scope, userId, scanSeverity: severity, findingsCount: findings.length }, ctx.taskId ?? null);

  const mediumFindings = findings.filter(f => f.severity === 'medium');
  let msg = `InstallSkill: installed "${skillName}" to ${scope} scope`;
  if (mediumFindings.length > 0) {
    const warnings = mediumFindings.map(f => `  - [medium] ${f.pattern}: "${f.match}" (line ${f.line})`).join('\n');
    msg += `\n\nWarnings (medium severity):\n${warnings}`;
  }
  return { output: msg, isError: false };
}

// ---------------------------------------------------------------------------
// Helpers for InstallSkillFromDir
// ---------------------------------------------------------------------------

interface DirStats {
  fileCount: number;
  totalBytes: number;
  maxDepth: number;
}

function getDirStats(dir: string, depth: number = 0): DirStats {
  const stats: DirStats = { fileCount: 0, totalBytes: 0, maxDepth: depth };
  let entries: string[];
  try {
    entries = readdirSync(dir);
  } catch {
    return stats;
  }
  for (const entry of entries) {
    const fullPath = join(dir, entry);
    let st;
    try {
      st = lstatSync(fullPath);
    } catch {
      continue;
    }
    if (st.isSymbolicLink()) continue;
    if (st.isDirectory()) {
      const sub = getDirStats(fullPath, depth + 1);
      stats.fileCount += sub.fileCount;
      stats.totalBytes += sub.totalBytes;
      if (sub.maxDepth > stats.maxDepth) stats.maxDepth = sub.maxDepth;
    } else if (st.isFile()) {
      stats.fileCount++;
      stats.totalBytes += st.size;
    }
  }
  return stats;
}

// ---------------------------------------------------------------------------
// Tool execution
// ---------------------------------------------------------------------------
export function executeSkillTool(
  name: string,
  input: Record<string, unknown>,
  ctx: ToolContext,
): ToolResult | null {
  if (name === 'InstallSkill') {
    return executeInstallSkill(input, ctx);
  }

  if (name === 'ListSkills') {
    const catalog = ctx.skillCatalog;
    if (!catalog) return { output: 'Error: skill catalog not available', isError: true };
    const userId = ctx.userId ?? 'local';
    const entries = catalog.getForUser(userId);
    if (entries.length === 0) return { output: 'No skills installed.', isError: false };
    const lines = entries.map(e => {
      const dirSuffix = 'dirPath' in e && e.dirPath ? ' (has scripts/)' : '';
      return `- **${e.name}** [${e.source}]: ${e.description}${dirSuffix}`;
    });
    return { output: lines.join('\n'), isError: false };
  }

  if (name !== 'ReadSkill') return null;

  const skillName = input['name'] as string;
  if (!skillName) {
    return { output: 'Error: name is required', isError: true };
  }

  const catalog = ctx.skillCatalog;
  if (!catalog) {
    return { output: 'Error: skill catalog not available', isError: true };
  }

  const userId = ctx.userId ?? 'local';
  const result = catalog.getSkillContent(skillName, userId);
  if (result === null) {
    const available = catalog.getForUser(userId).map(s => s.name).join(', ');
    return {
      output: `Skill "${skillName}" not found. Available skills: ${available || '(none)'}`,
      isError: true,
    };
  }

  if (result.dirPath) {
    const m = materializeSkill(result.dirPath, ctx.workspacePath, skillName);
    const loc = m.ok
      ? `このスキルのファイルは workspace の \`${m.relPath}/\` に配置しました（例: \`${m.relPath}/scripts/...\`）。スクリプトはこの相対パスで実行できます。`
      : `（注: スキルのファイルを workspace にコピーできませんでした: ${m.note}。SKILL.md の手順は以下を参照）`;
    return { output: `${loc}\n\n${result.content}`, isError: false };
  }
  return { output: result.content, isError: false };
}

