import { resolve, join } from 'path';
import { mkdirSync, readdirSync, readFileSync, writeFileSync, existsSync } from 'fs';
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
import { ToolDef } from '../../llm/openai-compat.js';
import type { ToolContext, ToolResult } from './core.js';

const BUILTIN_PIECES_DIR = resolve(process.cwd(), 'pieces');
const VALID_NAME = /^[a-z0-9-]+$/;

/** Normalise `string | string[] | undefined` → `string[]` (may be empty). */
function toCustomDirs(customDir: string | string[] | undefined): string[] {
  if (!customDir) return [];
  return Array.isArray(customDir) ? customDir : [customDir];
}

/**
 * Search all custom dirs in order, then fall back to built-in.
 * Returns the first path that exists, or null.
 */
function findPiecePath(name: string, customDir: string | string[] | undefined): string | null {
  for (const d of toCustomDirs(customDir)) {
    const p = join(d, `${name}.yaml`);
    if (existsSync(p)) return p;
  }
  const builtinPath = join(BUILTIN_PIECES_DIR, `${name}.yaml`);
  if (existsSync(builtinPath)) return builtinPath;
  return null;
}

/**
 * A piece is "built-in" when it lives only under the bundled BUILTIN_PIECES_DIR
 * (no override in any custom dir). Built-ins are git-tracked and shipped with the
 * app — letting the LLM rewrite them in place corrupts the install (a real
 * incident: the agent silently replaced game-tweet-generator with a version
 * missing max_movements, making every subsequent run abort instantly). The
 * LLM should use CreatePiece with a new name to derive a customized variant
 * instead.
 */
function isBuiltinOnly(name: string, customDir: string | string[] | undefined): boolean {
  for (const d of toCustomDirs(customDir)) {
    if (existsSync(join(d, `${name}.yaml`))) return false;
  }
  const builtinPath = join(BUILTIN_PIECES_DIR, `${name}.yaml`);
  return existsSync(builtinPath);
}

// --- Validation (same logic as pieces-api.ts) ---

function validatePiece(piece: any): string | null {
  if (!piece.name || !VALID_NAME.test(piece.name)) return 'name must be lowercase alphanumeric with hyphens';
  if (!piece.description) return 'description is required';
  if (!Array.isArray(piece.movements) || piece.movements.length === 0) return 'movements must be non-empty array';
  if (!piece.initial_movement) return 'initial_movement is required';
  // Required so the runtime loop has a hard ceiling. Without this a
  // forgotten/0/garbage value makes `while (steps < piece.max_movements)`
  // false on the first iteration → the run aborts immediately with
  // "Exceeded max movements (undefined)".
  if (typeof piece.max_movements !== 'number' || !Number.isFinite(piece.max_movements) || piece.max_movements <= 0) {
    return 'max_movements is required (positive integer, e.g. 50 for short tasks, 999 for open-ended ones)';
  }
  const names = new Set(piece.movements.map((m: any) => m.name));
  if (!names.has(piece.initial_movement)) return 'initial_movement must reference an existing movement';
  // Phase 6b: rules[].next only accepts existing movement names + WAIT_SUBTASKS.
  // Terminal moves (COMPLETE/ABORT/ASK) go through the `complete` tool now.
  // default_next is engine-internal and still accepts COMPLETE/ABORT/ASK.
  const validRuleNexts = new Set([...names, 'WAIT_SUBTASKS']);
  const validDefaultNexts = new Set([...names, 'COMPLETE', 'ABORT', 'ASK', 'WAIT_SUBTASKS']);
  for (const m of piece.movements) {
    if (!m.name) return 'each movement must have a name';
    if (m.default_next && !validDefaultNexts.has(m.default_next)) {
      return `movement "${m.name}": default_next "${m.default_next}" is invalid`;
    }
    if (Array.isArray(m.rules)) {
      for (const r of m.rules) {
        if (!validRuleNexts.has(r.next)) {
          if (r.next === 'COMPLETE' || r.next === 'ABORT' || r.next === 'ASK') {
            return `movement "${m.name}": rules[].next cannot be "${r.next}" (use the \`complete\` tool for terminal moves)`;
          }
          return `movement "${m.name}": rule next "${r.next}" is invalid`;
        }
      }
    }
  }
  return null;
}

// --- Tool definitions ---

const LIST_PIECES_DEF: ToolDef = {
  type: 'function',
  function: {
    name: 'ListPieces',
    description: '全 Piece（実行テンプレート: ツール制限・movement フロー制御）の一覧を取得する。Skill（参照知識）の一覧は ListSkills を使うこと。新規作成前に必ず実行。詳細は ReadToolDoc({ name: "ListPieces" })。',
    parameters: {
      type: 'object',
      properties: {},
      required: [],
    },
  },
};

const GET_PIECE_DEF: ToolDef = {
  type: 'function',
  function: {
    name: 'GetPiece',
    description: '指定 Piece（実行テンプレート）の完全な YAML 定義を取得する。Skill の全文取得には ReadSkill を使うこと。詳細は ReadToolDoc({ name: "GetPiece" })。',
    parameters: {
      type: 'object',
      properties: {
        name: { type: 'string', description: 'Piece 名（例: chat, general, research）' },
      },
      required: ['name'],
    },
  },
};

const CREATE_PIECE_DEF: ToolDef = {
  type: 'function',
  function: {
    name: 'CreatePiece',
    description: '新 Piece（実行テンプレート: movement + allowed_tools を定義）を YAML から作成する。Skill の追加には InstallSkill を使うこと。詳細は ReadToolDoc({ name: "CreatePiece" })。',
    parameters: {
      type: 'object',
      properties: {
        yaml_content: {
          type: 'string',
          description: 'Piece の完全な YAML 定義。name, description, initial_movement, movements を含むこと。',
        },
      },
      required: ['yaml_content'],
    },
  },
};

const UPDATE_PIECE_DEF: ToolDef = {
  type: 'function',
  function: {
    name: 'UpdatePiece',
    description: '既存 Piece を完全な YAML で全体置換する（差分更新ではない）。詳細は ReadToolDoc({ name: "UpdatePiece" })。',
    parameters: {
      type: 'object',
      properties: {
        name: { type: 'string', description: '更新対象の Piece 名' },
        yaml_content: {
          type: 'string',
          description: '更新後の完全な YAML 定義',
        },
      },
      required: ['name', 'yaml_content'],
    },
  },
};

export const TOOL_DEFS: Record<string, ToolDef> = {
  ListPieces: LIST_PIECES_DEF,
  GetPiece: GET_PIECE_DEF,
  CreatePiece: CREATE_PIECE_DEF,
  UpdatePiece: UPDATE_PIECE_DEF,
};

// --- Tool execution ---

function executeListPieces(ctx: ToolContext): ToolResult {
  try {
    const seen = new Set<string>();
    const pieces: Array<{ name: string; description: string; keywords: string[]; custom: boolean }> = [];
    const dirs: Array<{ dir: string; custom: boolean }> = [];
    for (const d of toCustomDirs(ctx.customPiecesDir)) {
      if (existsSync(d)) dirs.push({ dir: d, custom: true });
    }
    dirs.push({ dir: BUILTIN_PIECES_DIR, custom: false });

    for (const { dir, custom } of dirs) {
      const files = readdirSync(dir).filter(f => f.endsWith('.yaml'));
      for (const f of files) {
        const name = f.replace('.yaml', '');
        if (seen.has(name)) continue;
        seen.add(name);
        try {
          const raw = readFileSync(join(dir, f), 'utf-8');
          const p = parseYaml(raw);
          pieces.push({
            name: p.name ?? name,
            description: (p.description ?? '').split('\n')[0].trim(),
            keywords: p.triggers?.keywords ?? [],
            custom,
          });
        } catch {
          pieces.push({ name, description: '(parse error)', keywords: [], custom });
        }
      }
    }

    const lines = pieces.map(p => {
      const kw = p.keywords.length > 0 ? ` [keywords: ${p.keywords.join(', ')}]` : '';
      const tag = p.custom ? ' (custom)' : '';
      return `- ${p.name}: ${p.description}${kw}${tag}`;
    });
    return { output: `登録済み Piece (${pieces.length}件):\n${lines.join('\n')}`, isError: false };
  } catch (e) {
    return { output: `Failed to list pieces: ${(e as Error).message}`, isError: true };
  }
}

function executeGetPiece(input: Record<string, unknown>, ctx: ToolContext): ToolResult {
  const name = input['name'] as string;
  if (!name || !VALID_NAME.test(name)) {
    return { output: 'Invalid piece name. Use lowercase alphanumeric with hyphens.', isError: true };
  }
  const filePath = findPiecePath(name, ctx.customPiecesDir);
  if (!filePath) {
    return { output: `Piece "${name}" not found.`, isError: true };
  }
  try {
    const raw = readFileSync(filePath, 'utf-8');
    return { output: raw, isError: false };
  } catch (e) {
    return { output: `Failed to read piece: ${(e as Error).message}`, isError: true };
  }
}

function executeCreatePiece(input: Record<string, unknown>, ctx: ToolContext): ToolResult {
  const yamlContent = input['yaml_content'] as string;
  if (!yamlContent) {
    return { output: 'yaml_content is required.', isError: true };
  }

  let piece: any;
  try {
    piece = parseYaml(yamlContent);
  } catch (e) {
    return { output: `YAML parse error: ${(e as Error).message}`, isError: true };
  }

  const error = validatePiece(piece);
  if (error) {
    return { output: `Validation error: ${error}`, isError: true };
  }

  // 両ディレクトリで名前衝突確認
  if (findPiecePath(piece.name, ctx.customPiecesDir)) {
    return { output: `Piece "${piece.name}" already exists. Use UpdatePiece to modify it.`, isError: true };
  }

  // Write to the FIRST custom dir (per-user dir), or fall back to builtin if no custom dirs.
  const customDirs = toCustomDirs(ctx.customPiecesDir);
  const targetDir = customDirs[0] ?? BUILTIN_PIECES_DIR;
  mkdirSync(targetDir, { recursive: true });
  const filePath = join(targetDir, `${piece.name}.yaml`);
  try {
    writeFileSync(filePath, stringifyYaml(piece, { lineWidth: 120 }), 'utf-8');
    return { output: `Piece "${piece.name}" を作成しました。`, isError: false };
  } catch (e) {
    return { output: `Failed to create piece: ${(e as Error).message}`, isError: true };
  }
}

function executeUpdatePiece(input: Record<string, unknown>, ctx: ToolContext): ToolResult {
  const name = input['name'] as string;
  const yamlContent = input['yaml_content'] as string;

  if (!name || !VALID_NAME.test(name)) {
    return { output: 'Invalid piece name.', isError: true };
  }
  if (!yamlContent) {
    return { output: 'yaml_content is required.', isError: true };
  }

  // Refuse to overwrite git-tracked built-in pieces. Force the LLM to use
  // CreatePiece with a new name when it wants a customized variant.
  if (isBuiltinOnly(name, ctx.customPiecesDir)) {
    return {
      output: `Piece "${name}" は組み込み (built-in) のため UpdatePiece では編集できません。カスタマイズが必要なら CreatePiece で別名 (例: "${name}-custom") として新規作成してください。`,
      isError: true,
    };
  }

  const filePath = findPiecePath(name, ctx.customPiecesDir);
  if (!filePath) {
    return { output: `Piece "${name}" not found. Use CreatePiece to create it.`, isError: true };
  }

  let piece: any;
  try {
    piece = parseYaml(yamlContent);
  } catch (e) {
    return { output: `YAML parse error: ${(e as Error).message}`, isError: true };
  }

  piece.name = name;

  const error = validatePiece(piece);
  if (error) {
    return { output: `Validation error: ${error}`, isError: true };
  }

  try {
    writeFileSync(filePath, stringifyYaml(piece, { lineWidth: 120 }), 'utf-8');
    return { output: `Piece "${name}" を更新しました。`, isError: false };
  } catch (e) {
    return { output: `Failed to update piece: ${(e as Error).message}`, isError: true };
  }
}

export async function executeTool(
  name: string,
  input: Record<string, unknown>,
  ctx: ToolContext,
): Promise<ToolResult | null> {
  switch (name) {
    case 'ListPieces':
      return executeListPieces(ctx);
    case 'GetPiece':
      return executeGetPiece(input, ctx);
    case 'CreatePiece':
      return executeCreatePiece(input, ctx);
    case 'UpdatePiece':
      return executeUpdatePiece(input, ctx);
    default:
      return null;
  }
}
