import { createHash } from "node:crypto";
import { promises as fs } from "node:fs";
import { join } from "node:path";

import { VISION_INSPECT_ATTACHMENT_TOOL } from "./GoogleToolMode.ts";
import { workspaceRelPath } from "./geminiWorkspaceTools.ts";

export type VisionInspectAttachmentArgs = {
  attachment_id?: string;
  local_path?: string;
  path?: string;
  prompt?: string;
};

export type VisionInspectAttachmentResult = {
  summary: string;
  observations: string[];
  possible_issue: string;
  evidence: Array<{
    type: string;
    attachment_id?: string;
    path?: string;
    tool: string;
  }>;
  confidence: "low" | "medium" | "high";
};

type VisionGenerateContentResponse = {
  text?: string;
  candidates?: Array<{ content?: { parts?: Array<{ text?: string }> } }>;
};

export function isGoogleVisionTool(toolName: string): boolean {
  return toolName === VISION_INSPECT_ATTACHMENT_TOOL;
}

export function buildGoogleVisionToolDeclarations(): Array<{
  name: string;
  description: string;
  parameters: {
    type: string;
    properties: Record<string, unknown>;
    required?: string[];
  };
}> {
  return [
    {
      name: VISION_INSPECT_ATTACHMENT_TOOL,
      description:
        "Read-only vision inspection for fcop attachments or local image paths. " +
        "Returns structured summary, observations, and evidence for REPORT citation. " +
        "Does not modify lifecycle or create TASK/REPORT.",
      parameters: {
        type: "OBJECT",
        properties: {
          attachment_id: {
            type: "STRING",
            description: "Attachment id such as ATTACH-20260607-001",
          },
          local_path: {
            type: "STRING",
            description: "Relative or absolute path under project root",
          },
          path: {
            type: "STRING",
            description: "Alias of local_path",
          },
          prompt: {
            type: "STRING",
            description: "Optional inspection focus for the model",
          },
        },
      },
    },
  ];
}

async function resolveAttachmentAbsPath(
  projectRoot: string,
  args: VisionInspectAttachmentArgs,
): Promise<{ abs: string; rel: string; attachmentId?: string }> {
  const root = projectRoot;
  const direct = (args.local_path ?? args.path ?? "").trim();
  if (direct) {
    const abs = join(root, direct.replace(/^\//, ""));
    const rel = workspaceRelPath(root, abs);
    return { abs, rel };
  }

  const attachId = (args.attachment_id ?? "").trim();
  if (!attachId) {
    throw new Error(
      "vision.inspect_attachment requires attachment_id or local_path/path",
    );
  }

  const attachmentsRoot = join(root, "fcop", "attachments");
  let shardDirs: string[];
  try {
    shardDirs = await fs.readdir(attachmentsRoot);
  } catch {
    throw new Error(`attachments directory not found: fcop/attachments`);
  }

  const idLower = attachId.toLowerCase();
  for (const shard of shardDirs) {
    const shardDir = join(attachmentsRoot, shard);
    let stat;
    try {
      stat = await fs.stat(shardDir);
    } catch {
      continue;
    }
    if (!stat.isDirectory()) continue;
    let files: string[];
    try {
      files = await fs.readdir(shardDir);
    } catch {
      continue;
    }
    for (const name of files) {
      if (name.toLowerCase().includes(idLower)) {
        const abs = join(shardDir, name);
        const rel = workspaceRelPath(root, abs);
        return { abs, rel, attachmentId: attachId };
      }
    }
  }

  throw new Error(`attachment not found for id: ${attachId}`);
}

function mimeFromPath(filePath: string): string {
  const lower = filePath.toLowerCase();
  if (lower.endsWith(".png")) return "image/png";
  if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
  if (lower.endsWith(".gif")) return "image/gif";
  if (lower.endsWith(".webp")) return "image/webp";
  return "application/octet-stream";
}

export async function invokeGoogleVisionTool(
  projectRoot: string,
  toolName: string,
  args: Record<string, unknown>,
  opts?: {
    visionClient?: { models?: { generateContent?: (req: unknown) => Promise<unknown> } };
    visionModel?: string;
  },
): Promise<string> {
  if (!isGoogleVisionTool(toolName)) {
    throw new Error(`Not a vision tool: ${toolName}`);
  }

  const parsed: VisionInspectAttachmentArgs = {
    attachment_id:
      typeof args.attachment_id === "string" ? args.attachment_id : undefined,
    local_path: typeof args.local_path === "string" ? args.local_path : undefined,
    path: typeof args.path === "string" ? args.path : undefined,
    prompt: typeof args.prompt === "string" ? args.prompt : undefined,
  };

  const { abs, rel, attachmentId } = await resolveAttachmentAbsPath(
    projectRoot,
    parsed,
  );
  const stat = await fs.stat(abs);
  if (!stat.isFile()) {
    throw new Error(`attachment path is not a file: ${rel}`);
  }

  const buf = await fs.readFile(abs);
  const mime = mimeFromPath(abs);
  const sha = createHash("sha256").update(buf).digest("hex").slice(0, 16);
  const observations: string[] = [
    `file: ${rel}`,
    `size_bytes: ${stat.size}`,
    `mime: ${mime}`,
    `sha256_prefix: ${sha}`,
  ];

  let summary = `已读取附件 ${rel}（${stat.size} bytes, ${mime}）。`;
  let possibleIssue = "";
  let confidence: VisionInspectAttachmentResult["confidence"] = "low";

  const client = opts?.visionClient;
  const model = opts?.visionModel?.trim();
  if (client?.models?.generateContent && model && mime.startsWith("image/")) {
    const prompt =
      parsed.prompt?.trim() ||
      "Describe visible errors, UI issues, and evidence in this image. Reply in concise Chinese.";
    try {
      const response = (await client.models.generateContent({
        model,
        contents: [
          {
            role: "user",
            parts: [
              { text: prompt },
              {
                inlineData: {
                  mimeType: mime,
                  data: buf.toString("base64"),
                },
              },
            ],
          },
        ],
      })) as VisionGenerateContentResponse;

      const text =
        (typeof response.text === "string" && response.text) ||
        response.candidates?.[0]?.content?.parts
          ?.map((p) => p.text ?? "")
          .join("")
          .trim() ||
        "";

      if (text) {
        summary = text.split(/\n/)[0]?.trim() || summary;
        observations.push(...text.split(/\n/).filter(Boolean).slice(0, 8));
        possibleIssue = text.includes("错误") || text.toLowerCase().includes("error")
          ? "模型在图像中观察到潜在异常，请人工复核。"
          : "";
        confidence = text.length > 80 ? "medium" : "low";
      }
    } catch (err) {
      observations.push(
        `vision_model_error: ${err instanceof Error ? err.message : String(err)}`,
      );
    }
  }

  const payload: VisionInspectAttachmentResult = {
    summary,
    observations,
    possible_issue: possibleIssue,
    evidence: [
      {
        type: mime.startsWith("image/") ? "image" : "file",
        attachment_id: attachmentId,
        path: rel,
        tool: VISION_INSPECT_ATTACHMENT_TOOL,
      },
    ],
    confidence,
  };

  return JSON.stringify({ ok: true, result: payload });
}
