import * as fs from 'fs';
import * as path from 'path';
import sharp from 'sharp';
import { ToolDef } from '../../llm/openai-compat.js';
import type { ToolContext, ToolResult } from './core.js';
import { resolveAndGuard, resolveOutputPathWithin } from './core.js';
import { logger } from '../../logger.js';

// --- Supported image extensions ---

const SUPPORTED_EXTENSIONS = new Set(['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp']);
const DEFAULT_IMAGE_PROMPT = '画像の内容を日本語で説明してください。';

// Normalize extension for MIME type (jpg -> jpeg)
function extToMime(ext: string): string {
  if (ext === 'jpg') return 'jpeg';
  return ext;
}

// --- SVG annotation helpers ---

interface Annotation {
  type: 'rectangle' | 'arrow' | 'text';
  x?: number;
  y?: number;
  width?: number;
  height?: number;
  from_x?: number;
  from_y?: number;
  to_x?: number;
  to_y?: number;
  text?: string;
  color?: string;
  label?: string;
  font_size?: number;
}

function computeScaling(imageWidth: number, imageHeight: number): { strokeWidth: number; fontSize: number } {
  const shortSide = Math.min(imageWidth, imageHeight);
  return {
    strokeWidth: Math.max(2, Math.min(5, Math.round(shortSide / 400))),
    fontSize: Math.max(12, Math.min(32, Math.round(shortSide / 40))),
  };
}

function escapeXml(str: string): string {
  return str
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&apos;');
}

// Convert a CSS color value into a string safe for use as an SVG `id`.
// SVG ids referenced via `url(#...)` may not contain spaces, parens, or commas,
// so functional notations like `rgb(255, 0, 0)` cannot be used as-is.
function colorToMarkerId(color: string): string {
  return `ah-${color.replace(/[^a-zA-Z0-9]/g, '_')}`;
}

function buildAnnotationSvg(
  imageWidth: number,
  imageHeight: number,
  annotations: Annotation[],
): string {
  const { strokeWidth, fontSize: defaultFontSize } = computeScaling(imageWidth, imageHeight);
  const markerSize = strokeWidth * 3;
  const parts: string[] = [];

  parts.push(`<svg xmlns="http://www.w3.org/2000/svg" width="${imageWidth}" height="${imageHeight}">`);
  parts.push('<defs>');
  // One arrowhead marker per unique color
  const arrowColors = new Set<string>();
  for (const a of annotations) {
    if (a.type === 'arrow') arrowColors.add(a.color ?? '#FF0000');
  }
  for (const color of arrowColors) {
    const id = colorToMarkerId(color);
    parts.push(
      `<marker id="${id}" markerWidth="${markerSize}" markerHeight="${markerSize * 0.7}" ` +
      `refX="${markerSize}" refY="${markerSize * 0.35}" orient="auto">` +
      `<polygon points="0 0, ${markerSize} ${markerSize * 0.35}, 0 ${markerSize * 0.7}" fill="${color}"/>` +
      '</marker>',
    );
  }
  parts.push('</defs>');

  for (const a of annotations) {
    const color = a.color ?? '#FF0000';
    const fontSize = a.font_size ?? defaultFontSize;
    const labelPadding = Math.round(fontSize * 0.3);

    switch (a.type) {
      case 'rectangle': {
        const x = a.x ?? 0;
        const y = a.y ?? 0;
        const w = a.width ?? 50;
        const h = a.height ?? 50;
        parts.push(
          `<rect x="${x}" y="${y}" width="${w}" height="${h}" ` +
          `fill="none" stroke="${color}" stroke-width="${strokeWidth}"/>`,
        );
        if (a.label) {
          const labelWidth = a.label.length * fontSize * 0.6 + labelPadding * 2;
          const labelHeight = fontSize + labelPadding * 2;
          const labelY = y - labelHeight;
          const textY = y - labelPadding;
          parts.push(
            `<rect x="${x}" y="${labelY}" width="${labelWidth}" height="${labelHeight}" ` +
            `fill="${color}" rx="2"/>`,
          );
          parts.push(
            `<text x="${x + labelPadding}" y="${textY}" ` +
            `fill="white" font-size="${fontSize}" font-family="sans-serif">${escapeXml(a.label)}</text>`,
          );
        }
        break;
      }
      case 'arrow': {
        const fx = a.from_x ?? 0;
        const fy = a.from_y ?? 0;
        const tx = a.to_x ?? 0;
        const ty = a.to_y ?? 0;
        const markerId = colorToMarkerId(color);
        parts.push(
          `<line x1="${fx}" y1="${fy}" x2="${tx}" y2="${ty}" ` +
          `stroke="${color}" stroke-width="${strokeWidth}" marker-end="url(#${markerId})"/>`,
        );
        if (a.label) {
          const bgWidth = a.label.length * fontSize * 0.6 + labelPadding * 2;
          const bgHeight = fontSize + labelPadding * 2;
          parts.push(
            `<rect x="${fx}" y="${fy - bgHeight}" width="${bgWidth}" height="${bgHeight}" ` +
            `fill="rgba(0,0,0,0.6)" rx="3"/>`,
          );
          parts.push(
            `<text x="${fx + labelPadding}" y="${fy - labelPadding}" ` +
            `fill="${color}" font-size="${fontSize}" font-family="sans-serif">${escapeXml(a.label)}</text>`,
          );
        }
        break;
      }
      case 'text': {
        const x = a.x ?? 0;
        const y = a.y ?? 0;
        const text = a.text ?? '';
        if (text) {
          const bgWidth = text.length * fontSize * 0.6 + labelPadding * 2;
          const bgHeight = fontSize + labelPadding * 2;
          parts.push(
            `<rect x="${x - labelPadding}" y="${y - fontSize}" width="${bgWidth}" height="${bgHeight}" ` +
            `fill="rgba(0,0,0,0.6)" rx="3"/>`,
          );
          parts.push(
            `<text x="${x}" y="${y}" ` +
            `fill="${color}" font-size="${fontSize}" font-family="sans-serif">${escapeXml(text)}</text>`,
          );
        }
        break;
      }
    }
  }

  parts.push('</svg>');
  return parts.join('\n');
}

// --- Tool definition ---

const READIMAGE_DEF: ToolDef = {
  type: 'function',
  function: {
    name: 'ReadImage',
    description: '画像ファイルを LLM に直接渡して内容を認識させる（VLM 対応 worker のみ）。詳細は ReadToolDoc({ name: "ReadImage" })。',
    parameters: {
      type: 'object',
      properties: {
        file_path: { type: 'string', description: 'workspace 内の画像ファイルパス' },
        prompt: { type: 'string', description: '画像について質問したいこと（省略可）' },
      },
      required: ['file_path'],
    },
  },
};

const ANNOTATEIMAGE_DEF: ToolDef = {
  type: 'function',
  function: {
    name: 'AnnotateImage',
    description: '画像上に矩形枠・矢印・テキストラベルを描画して output/ に保存する（元画像は変更されない）。詳細は ReadToolDoc({ name: "AnnotateImage" })。',
    parameters: {
      type: 'object',
      properties: {
        input_path: { type: 'string', description: 'workspace 内の元画像ファイルパス' },
        output_path: { type: 'string', description: '出力先パス（output/ 配下）' },
        annotations: {
          type: 'array',
          description: '描画する注釈の配列',
          items: {
            type: 'object',
            properties: {
              type: { type: 'string', enum: ['rectangle', 'arrow', 'text'], description: '注釈の種類' },
              x: { type: 'number', description: 'rectangle/text: 左上 X 座標' },
              y: { type: 'number', description: 'rectangle/text: 左上 Y 座標' },
              width: { type: 'number', description: 'rectangle: 幅' },
              height: { type: 'number', description: 'rectangle: 高さ' },
              from_x: { type: 'number', description: 'arrow: 始点 X' },
              from_y: { type: 'number', description: 'arrow: 始点 Y' },
              to_x: { type: 'number', description: 'arrow: 終点 X' },
              to_y: { type: 'number', description: 'arrow: 終点 Y' },
              text: { type: 'string', description: 'text: 表示するテキスト' },
              color: { type: 'string', description: '色（デフォルト: #FF0000）' },
              label: { type: 'string', description: 'rectangle/arrow: ラベルテキスト' },
              font_size: { type: 'number', description: 'フォントサイズ（省略時は画像サイズから自動算出）' },
            },
            required: ['type'],
          },
        },
      },
      required: ['input_path', 'output_path', 'annotations'],
    },
  },
};

export const TOOL_DEFS: Record<string, ToolDef> = {
  ReadImage: READIMAGE_DEF,
  AnnotateImage: ANNOTATEIMAGE_DEF,
};

// --- Tool execution ---

export function resolveImagePath(
  filePath: string,
  ctx: ToolContext,
): ToolResult | { resolved: string; ext: string; dataUrl: string } {
  let resolved: string;
  try {
    resolved = resolveAndGuard(ctx.workspacePath, filePath);
  } catch (e) {
    return { output: (e as Error).message, isError: true };
  }

  const ext = path.extname(resolved).replace('.', '').toLowerCase();
  if (!SUPPORTED_EXTENSIONS.has(ext)) {
    return {
      output: `Unsupported image extension: "${ext}". Supported: ${[...SUPPORTED_EXTENSIONS].join(', ')}`,
      isError: true,
    };
  }

  let base64data: string;
  try {
    const buf = fs.readFileSync(resolved);
    base64data = buf.toString('base64');
  } catch (e) {
    return { output: `Failed to read image: ${(e as Error).message}`, isError: true };
  }

  const mimeExt = extToMime(ext);
  return {
    resolved,
    ext,
    dataUrl: `data:image/${mimeExt};base64,${base64data}`,
  };
}

export async function callVisionModel(
  dataUrl: string,
  prompt: string,
  ctx: ToolContext,
): Promise<ToolResult> {
  const finalPrompt = prompt.trim() || DEFAULT_IMAGE_PROMPT;

  const toolsConfig = ctx.toolsConfig ?? {};
  const visionModel = toolsConfig.visionModel ?? 'qwen2-vl:8b-instruct';
  const visionBaseUrl = toolsConfig.visionBaseUrl ?? 'http://10.0.0.10:11434/v1';
  const visionTimeout = (toolsConfig.visionTimeout ?? 60) * 1000;
  const visionMaxTokens = toolsConfig.visionMaxTokens ?? 1024;
  const requestBody = {
    model: visionModel,
    messages: [
      {
        role: 'user',
        content: [
          { type: 'text', text: finalPrompt },
          { type: 'image_url', image_url: { url: dataUrl } },
        ],
      },
    ],
    max_tokens: visionMaxTokens,
  };

  logger.debug(`[ReadImage] calling ${visionBaseUrl}/chat/completions model=${visionModel}`);

  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), visionTimeout);

  try {
    const response = await fetch(`${visionBaseUrl}/chat/completions`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(requestBody),
      signal: controller.signal,
    });

    if (!response.ok) {
      const text = await response.text().catch(() => '');
      return { output: `Vision API error ${response.status}: ${text}`, isError: true };
    }

    const json = (await response.json()) as {
      choices?: Array<{ message?: { content?: string } }>;
    };

    const content = json.choices?.[0]?.message?.content;
    if (typeof content !== 'string') {
      return { output: 'Vision API returned no content', isError: true };
    }

    return { output: content, isError: false };
  } catch (e) {
    if ((e as Error).name === 'AbortError') {
      return { output: `Vision API timed out after ${visionTimeout / 1000}s`, isError: true };
    }
    return { output: `Vision API request failed: ${(e as Error).message}`, isError: true };
  } finally {
    clearTimeout(timer);
  }
}

async function executeReadImage(
  input: Record<string, unknown>,
  ctx: ToolContext,
): Promise<ToolResult> {
  if (!ctx.vlmEnabled) {
    return { output: 'ReadImage requires VLM-enabled worker (vlm: true)', isError: true };
  }

  const filePath = input['file_path'] as string;
  const prompt = typeof input['prompt'] === 'string' ? input['prompt'] : '';

  const resolvedImage = resolveImagePath(filePath, ctx);
  if ('isError' in resolvedImage) return resolvedImage;

  const description = prompt
    ? `画像を読み込みました: ${filePath}\n指示: ${prompt}`
    : `画像を読み込みました: ${filePath}`;

  return {
    output: description,
    isError: false,
    images: [{ dataUrl: resolvedImage.dataUrl, label: filePath }],
  };
}

function isValidCssColor(color: string): boolean {
  if (typeof color !== 'string') return false;
  // Hex: #RGB, #RRGGBB, #RRGGBBAA
  if (/^#[0-9A-Fa-f]{3}$|^#[0-9A-Fa-f]{6}$|^#[0-9A-Fa-f]{8}$/.test(color)) return true;
  // Named colors (lowercase letters only — covers "red", "blue", "transparent", etc.)
  if (/^[a-z]+$/.test(color)) return true;
  // rgb(), rgba(), hsl(), hsla() — basic shape only
  if (/^(?:rgb|rgba|hsl|hsla)\(\s*[\d.,%\s/]+\s*\)$/.test(color)) return true;
  return false;
}

const VALID_TYPES = new Set(['rectangle', 'arrow', 'text']);

async function executeAnnotateImage(
  input: Record<string, unknown>,
  ctx: ToolContext,
): Promise<ToolResult> {
  if (!ctx.editAllowed) {
    return { output: 'AnnotateImage is not allowed: edit flag is false', isError: true };
  }

  const inputPath = input['input_path'] as string;
  const outputPath = input['output_path'] as string;
  const annotations = input['annotations'] as Annotation[];

  if (!Array.isArray(annotations) || annotations.length === 0) {
    return { output: 'AnnotateImage requires at least one annotation', isError: true };
  }

  for (let i = 0; i < annotations.length; i++) {
    const a = annotations[i];
    if (!VALID_TYPES.has(a.type)) {
      return {
        output: `AnnotateImage failed: annotation[${i}].type "${a.type}" is invalid. Must be one of: rectangle, arrow, text.`,
        isError: true,
      };
    }
    if (a.color !== undefined && !isValidCssColor(a.color)) {
      return {
        output: `AnnotateImage failed: annotation[${i}] has invalid color "${a.color}". Use formats like "#FF0000", "red", or "rgb(255,0,0)".`,
        isError: true,
      };
    }
  }

  // Resolve and guard input path
  let resolvedInput: string;
  try {
    resolvedInput = resolveAndGuard(ctx.workspacePath, inputPath);
  } catch (e) {
    return { output: (e as Error).message, isError: true };
  }

  // Resolve and guard output path (must be within output/)
  let resolvedOutput: string;
  try {
    resolvedOutput = resolveOutputPathWithin(ctx.workspacePath, outputPath, ['output']);
  } catch (e) {
    return { output: (e as Error).message, isError: true };
  }

  try {
    // Read image metadata
    const metadata = await sharp(resolvedInput).metadata();
    if (!metadata.width || !metadata.height) {
      return {
        output: `AnnotateImage failed: cannot determine image dimensions for ${inputPath}`,
        isError: true,
      };
    }
    const imageWidth = metadata.width;
    const imageHeight = metadata.height;

    // Build SVG overlay
    const svg = buildAnnotationSvg(imageWidth, imageHeight, annotations);
    const svgBuffer = Buffer.from(svg);

    // Composite SVG onto the original image and save as PNG
    fs.mkdirSync(path.dirname(resolvedOutput), { recursive: true });
    await sharp(resolvedInput)
      .composite([{ input: svgBuffer, top: 0, left: 0 }])
      .png()
      .toFile(resolvedOutput);

    const count = annotations.length;
    return {
      output: `Annotated image saved to ${outputPath} (${count} annotation${count !== 1 ? 's' : ''} applied)`,
      isError: false,
    };
  } catch (e) {
    return { output: `AnnotateImage failed: ${(e as Error).message}`, isError: true };
  }
}

export async function executeTool(
  name: string,
  input: Record<string, unknown>,
  ctx: ToolContext,
): Promise<ToolResult | null> {
  switch (name) {
    case 'ReadImage':
      return executeReadImage(input, ctx);
    case 'AnnotateImage':
      return executeAnnotateImage(input, ctx);
    default:
      return null;
  }
}
