/**
 * Prompt coach — on-demand, context-aware evaluation of a draft task prompt
 * before it is submitted from the create dialog.
 *
 * Goal: teach the user to write better prompts. Given a draft instruction we
 * read the user's own context (memory index, AGENTS.md, owned skills, visible
 * pieces) and ask a cheap model to score the draft, rewrite it, predict the
 * piece, and surface MAESTRO features the user might not know about.
 *
 * This module is deliberately free of HTTP / LLM-client wiring: the LLM call
 * is injected (`deps.callLlm`) so the prompt assembly and result normalization
 * are unit-testable without a backend. The route handler in
 * bridge/local-tasks-api.ts builds the concrete `callLlm` over the cheap
 * `titleClient`.
 */

export interface PromptCoachAxis {
  name: string;
  score: number; // 0-10
  comment: string;
}

export interface PromptCoachPiecePrediction {
  name: string;
  reason: string;
}

export interface PromptCoachTip {
  feature: string;
  suggestion: string;
}

export interface PromptCoachResult {
  /** Overall draft quality, 0-100. */
  overall: number;
  /** Per-axis scores: 明確さ / 具体性 / コンテキスト / 成果物指定. */
  axes: PromptCoachAxis[];
  /** A rewritten, improved version of the draft (Japanese). */
  rewrite: string;
  /** Predicted piece, constrained to the user's visible catalog, or null. */
  predicted_piece: PromptCoachPiecePrediction | null;
  /** MAESTRO feature suggestions (attachments, schedules, SSH, knowledge…). */
  maestro_tips: PromptCoachTip[];
  /** Notes grounded in the user's memory / AGENTS.md / skills. */
  personalized: string[];
}

export interface PromptCoachRequest {
  instruction: string;
  /** Optional piece the user pre-selected in the dialog ('auto' = unset). */
  piece?: string;
  userId: string;
  /** Aborts the underlying LLM call when the route times out. */
  signal?: AbortSignal;
}

/** Minimal shape of the catalogs the coach reads (name + description). */
export interface CatalogLike {
  getForUser(userId: string): Array<{ name: string; description: string }>;
}

export interface PromptCoachDeps {
  userFolderRoot: string;
  pieceCatalog: CatalogLike;
  skillCatalog: CatalogLike;
  /**
   * Performs the actual cheap-model call. Receives the assembled prompts and
   * returns the raw parsed tool arguments (the model is forced to call the
   * submit_evaluation tool). Throws on LLM error.
   */
  callLlm: (args: {
    system: string;
    user: string;
    userId: string;
    signal?: AbortSignal;
  }) => Promise<Record<string, unknown>>;
  /** Reads MEMORY.md one-line index. Injectable for tests. */
  readMemory?: (rootDir: string, userId: string) => string | null;
  /** Reads the user's AGENTS.md. Injectable for tests. */
  readAgents?: (rootDir: string, userId: string) => string | null;
}

/** Size caps so the bundle fits a cheap model's context window. */
export const COACH_LIMITS = {
  instructionChars: 8000,
  memoryChars: 4000,
  agentsChars: 4000,
  skillDescChars: 120,
  maxSkills: 60,
  pieceDescChars: 140,
  maxPieces: 80,
} as const;

function truncate(s: string, max: number): string {
  if (s.length <= max) return s;
  return `${s.slice(0, max)}\n…[truncated]`;
}

const SYSTEM_PROMPT = [
  'あなたは MAESTRO（自律エージェント実行基盤）のプロンプト改善コーチです。',
  'ユーザーがタスクを送信する前の下書きを評価し、より良い書き方を教えて、プロンプト力を育てるのが目的です。',
  '出力は必ず submit_evaluation ツールを 1 回だけ呼んで返してください。すべて日本語で書きます。',
  '',
  '評価の観点（axes）は次の 4 つに固定:',
  '- 明確さ: 何をしてほしいかが曖昧でないか',
  '- 具体性: 対象・条件・範囲が具体的か',
  '- コンテキスト: 判断に必要な前提・背景が与えられているか',
  '- 成果物指定: 出力フォーマット・粒度・宛先が指定されているか',
  '各 axis は 0-10、overall は 0-100 で採点します。',
  '',
  'rewrite には、ユーザーがそのまま使える改善済みの依頼文を入れます。',
  'predicted_piece は、与えられた piece 候補の中から最も適切なものを 1 つ選び、name は候補名と完全一致させます。該当が無ければ null。',
  'maestro_tips では、この下書きで活かせる MAESTRO 機能（ファイル添付 / 定期実行スケジュール / SSH 操作 / ナレッジ検索 / サブタスク並列実行 など）を提案します。当てはまらなければ空配列。',
  'personalized では、ユーザーの memory / AGENTS.md / 所有スキルを踏まえた指摘を入れます（例: 「該当スキルを既に所有しているので言及不要」「AGENTS.md の方針と矛盾」「memory にある前提は省略可能」）。根拠が無ければ空配列。',
].join('\n');

/**
 * Builds the system + user prompts and the list of candidate piece names.
 * Pure: depends only on the injected catalogs/readers, never on globals.
 */
export function buildCoachMessages(
  deps: PromptCoachDeps,
  req: PromptCoachRequest,
): { system: string; user: string; pieceCandidates: string[] } {
  const readMemory = deps.readMemory ?? (() => null);
  const readAgents = deps.readAgents ?? (() => null);

  const memory = readMemory(deps.userFolderRoot, req.userId);
  const agents = readAgents(deps.userFolderRoot, req.userId);
  const skills = deps.skillCatalog.getForUser(req.userId).slice(0, COACH_LIMITS.maxSkills);
  const pieces = deps.pieceCatalog.getForUser(req.userId).slice(0, COACH_LIMITS.maxPieces);
  const pieceCandidates = pieces.map((p) => p.name);

  const sections: string[] = [];

  sections.push('## 評価対象の下書き');
  sections.push(truncate(req.instruction.trim(), COACH_LIMITS.instructionChars) || '(空)');

  if (req.piece && req.piece !== 'auto') {
    sections.push('## ユーザーが選択中の piece');
    sections.push(req.piece);
  }

  if (memory && memory.trim()) {
    sections.push('## ユーザーの memory（MEMORY.md 一覧）');
    sections.push(truncate(memory.trim(), COACH_LIMITS.memoryChars));
  }

  if (agents && agents.trim()) {
    sections.push('## ユーザーの AGENTS.md（作業方針）');
    sections.push(truncate(agents.trim(), COACH_LIMITS.agentsChars));
  }

  if (skills.length > 0) {
    sections.push('## ユーザーが利用できるスキル');
    sections.push(
      skills
        .map((s) => `- ${s.name}: ${truncate((s.description ?? '').trim(), COACH_LIMITS.skillDescChars)}`)
        .join('\n'),
    );
  }

  if (pieces.length > 0) {
    sections.push('## piece 候補（predicted_piece.name はこの中から選ぶ）');
    sections.push(
      pieces
        .map((p) => `- ${p.name}: ${truncate((p.description ?? '').trim(), COACH_LIMITS.pieceDescChars)}`)
        .join('\n'),
    );
  } else {
    sections.push('## piece 候補');
    sections.push('(候補なし。predicted_piece は null にする)');
  }

  return { system: SYSTEM_PROMPT, user: sections.join('\n\n'), pieceCandidates };
}

function clampNumber(v: unknown, min: number, max: number, fallback: number): number {
  const n = typeof v === 'number' && Number.isFinite(v) ? v : Number(v);
  if (!Number.isFinite(n)) return fallback;
  return Math.max(min, Math.min(max, Math.round(n)));
}

function asString(v: unknown): string {
  return typeof v === 'string' ? v : '';
}

/**
 * Validates and clamps the raw LLM tool arguments into a PromptCoachResult.
 * Never throws on malformed input — fills safe defaults instead — so a quirky
 * model response degrades gracefully rather than 500-ing the route.
 */
export function normalizeCoachResult(
  raw: Record<string, unknown>,
  pieceCandidates: string[],
): PromptCoachResult {
  const axes: PromptCoachAxis[] = Array.isArray(raw.axes)
    ? raw.axes.slice(0, 4).map((a) => {
        const o = (a ?? {}) as Record<string, unknown>;
        return {
          name: asString(o.name),
          score: clampNumber(o.score, 0, 10, 0),
          comment: asString(o.comment),
        };
      })
    : [];

  let predicted: PromptCoachPiecePrediction | null = null;
  const rawPiece = raw.predicted_piece as Record<string, unknown> | null | undefined;
  if (rawPiece && typeof rawPiece === 'object') {
    const name = asString(rawPiece.name);
    if (name && pieceCandidates.includes(name)) {
      predicted = { name, reason: asString(rawPiece.reason) };
    }
  }

  const maestroTips: PromptCoachTip[] = Array.isArray(raw.maestro_tips)
    ? raw.maestro_tips
        .map((t) => {
          const o = (t ?? {}) as Record<string, unknown>;
          return { feature: asString(o.feature), suggestion: asString(o.suggestion) };
        })
        .filter((t) => t.feature || t.suggestion)
    : [];

  const personalized: string[] = Array.isArray(raw.personalized)
    ? raw.personalized.map(asString).filter((s) => s.length > 0)
    : [];

  return {
    overall: clampNumber(raw.overall, 0, 100, 0),
    axes,
    rewrite: asString(raw.rewrite),
    predicted_piece: predicted,
    maestro_tips: maestroTips,
    personalized,
  };
}

/** Assembles the bundle, runs the injected LLM call, normalizes the result. */
export async function runPromptCoach(
  deps: PromptCoachDeps,
  req: PromptCoachRequest,
): Promise<PromptCoachResult> {
  const { system, user, pieceCandidates } = buildCoachMessages(deps, req);
  const raw = await deps.callLlm({ system, user, userId: req.userId, signal: req.signal });
  return normalizeCoachResult(raw ?? {}, pieceCandidates);
}

/** OpenAI tools-format schema. The model is forced to call this. */
export const PROMPT_COACH_TOOL_SCHEMA = {
  type: 'function',
  function: {
    name: 'submit_evaluation',
    description: 'Submit the evaluation of the draft task prompt.',
    parameters: {
      type: 'object',
      additionalProperties: false,
      required: ['overall', 'axes', 'rewrite', 'predicted_piece', 'maestro_tips', 'personalized'],
      properties: {
        overall: { type: 'number', minimum: 0, maximum: 100 },
        axes: {
          type: 'array',
          maxItems: 4,
          items: {
            type: 'object',
            additionalProperties: false,
            required: ['name', 'score', 'comment'],
            properties: {
              name: { type: 'string', maxLength: 40 },
              score: { type: 'number', minimum: 0, maximum: 10 },
              comment: { type: 'string', maxLength: 400 },
            },
          },
        },
        rewrite: { type: 'string', maxLength: 4000 },
        predicted_piece: {
          type: ['object', 'null'],
          additionalProperties: false,
          required: ['name', 'reason'],
          properties: {
            name: { type: 'string', maxLength: 80 },
            reason: { type: 'string', maxLength: 400 },
          },
        },
        maestro_tips: {
          type: 'array',
          maxItems: 6,
          items: {
            type: 'object',
            additionalProperties: false,
            required: ['feature', 'suggestion'],
            properties: {
              feature: { type: 'string', maxLength: 60 },
              suggestion: { type: 'string', maxLength: 400 },
            },
          },
        },
        personalized: {
          type: 'array',
          maxItems: 8,
          items: { type: 'string', maxLength: 400 },
        },
      },
    },
  },
} as const;
