/**
 * MCP gno_capture tool - create a new document.
 *
 * @module src/mcp/tools/capture
 */

// node:fs/promises for mkdir (no Bun equivalent for structure ops)
import { mkdir } from "node:fs/promises";
// node:path for path utils (no Bun path utils)
import { dirname, extname, join } from "node:path";

import type { ToolContext } from "../server";

import { buildUri } from "../../app/constants";
import { MCP_ERRORS } from "../../core/errors";
import { withWriteLock } from "../../core/file-lock";
import { atomicWrite } from "../../core/file-ops";
import {
  resolveNoteCreatePlan,
  type NoteCollisionPolicy,
} from "../../core/note-creation";
import {
  getNotePreset,
  resolveNotePreset,
  type NotePresetId,
} from "../../core/note-presets";
import { normalizeTag, validateTag } from "../../core/tags";
import {
  normalizeCollectionName,
  validateRelPath,
} from "../../core/validation";
import { defaultSyncService } from "../../ingestion";
import { updateFrontmatterTags } from "../../ingestion/frontmatter";
import { extractTitle } from "../../pipeline/contextual";
import { runTool, type ToolResult } from "./index";

interface CaptureInput {
  collection: string;
  content?: string;
  title?: string;
  path?: string;
  overwrite?: boolean;
  folderPath?: string;
  collisionPolicy?: NoteCollisionPolicy;
  presetId?: NotePresetId;
  tags?: string[];
}

interface CaptureResult {
  docid: string;
  uri: string;
  absPath: string;
  collection: string;
  relPath: string;
  created: boolean;
  overwritten: boolean;
  serverInstanceId: string;
  tags?: string[];
}

const SENSITIVE_SUBPATHS = new Set([
  ".ssh",
  ".gnupg",
  ".aws",
  ".config",
  ".git",
  "node_modules",
]);

function sanitizeFilename(title: string): string {
  return title
    .toLowerCase()
    .trim()
    .replaceAll(/[^\w\s-]/g, "")
    .replaceAll(/\s+/g, "-")
    .replaceAll(/-+/g, "-")
    .replace(/^-|-$/g, "");
}

function ensureMarkdownExtension(relPath: string): string {
  return extname(relPath) ? relPath : `${relPath}.md`;
}

function assertNotSensitive(relPath: string): void {
  const firstSegment = relPath.split(/[\\/]/)[0];
  if (firstSegment && SENSITIVE_SUBPATHS.has(firstSegment)) {
    throw new Error(
      `${MCP_ERRORS.INVALID_PATH.code}: Cannot write to sensitive directory: ${firstSegment}`
    );
  }
}

function generateFilename(title: string | undefined, content: string): string {
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
  const fallback = `note-${timestamp}.md`;
  const baseTitle = title?.trim() || extractTitle(content, fallback);
  const slug = sanitizeFilename(baseTitle);
  const safeSlug = slug || `note-${timestamp}`;
  return `${safeSlug}.md`;
}

function formatCaptureResult(result: CaptureResult): string {
  const lines: string[] = [];
  lines.push(`Doc: ${result.docid}`);
  lines.push(`URI: ${result.uri}`);
  lines.push(`Path: ${result.absPath}`);
  lines.push(`Created: ${result.created ? "yes" : "no"}`);
  lines.push(`Overwritten: ${result.overwritten ? "yes" : "no"}`);
  if (result.tags && result.tags.length > 0) {
    lines.push(`Tags: ${result.tags.join(", ")}`);
  }
  return lines.join("\n");
}

export function handleCapture(
  args: CaptureInput,
  ctx: ToolContext
): Promise<ToolResult> {
  return runTool(
    ctx,
    "gno_capture",
    async () => {
      if (!ctx.enableWrite) {
        throw new Error("Write tools disabled. Start MCP with --enable-write.");
      }

      return await withWriteLock(ctx.writeLockPath, async () => {
        const collectionName = normalizeCollectionName(args.collection);
        const collection = ctx.collections.find(
          (c) => c.name.toLowerCase() === collectionName
        );
        if (!collection) {
          throw new Error(
            `${MCP_ERRORS.NOT_FOUND.code}: Collection not found: ${args.collection}`
          );
        }

        if (args.presetId && !getNotePreset(args.presetId)) {
          throw new Error(
            `${MCP_ERRORS.INVALID_INPUT.code}: Unknown presetId: ${args.presetId}`
          );
        }

        const existingDocs = await ctx.store.listDocuments(collectionName);
        if (!existingDocs.ok) {
          throw new Error(existingDocs.error.message);
        }

        let relPath: string;
        try {
          const plan = resolveNoteCreatePlan(
            {
              collection: collection.name,
              relPath: args.path
                ? ensureMarkdownExtension(validateRelPath(args.path))
                : undefined,
              title:
                args.title?.trim() ||
                generateFilename(args.title, args.content ?? "").replace(
                  /\.md$/u,
                  ""
                ),
              folderPath: args.folderPath,
              collisionPolicy: args.collisionPolicy,
            },
            existingDocs.value.map((doc) => doc.relPath)
          );
          if (plan.openedExisting) {
            throw new Error(
              `${MCP_ERRORS.CONFLICT.code}: Existing note resolution is not supported through gno_capture yet`
            );
          }
          relPath = plan.relPath;
        } catch (error) {
          const message =
            error instanceof Error ? error.message : String(error);
          throw new Error(`${MCP_ERRORS.INVALID_PATH.code}: ${message}`);
        }

        assertNotSensitive(relPath);

        // Validate and normalize tags
        let normalizedTags: string[] = [];
        if (args.tags && args.tags.length > 0) {
          for (const tag of args.tags) {
            const normalized = normalizeTag(tag);
            if (!validateTag(normalized)) {
              throw new Error(
                `${MCP_ERRORS.INVALID_INPUT.code}: Invalid tag "${tag}". Tags must be lowercase, alphanumeric with hyphens/dots/slashes.`
              );
            }
            normalizedTags.push(normalized);
          }
          // Dedupe
          normalizedTags = [...new Set(normalizedTags)];
        }

        const absPath = join(collection.path, relPath);
        const file = Bun.file(absPath);
        const exists = await file.exists();
        if (exists && !args.overwrite) {
          throw new Error(
            `${MCP_ERRORS.CONFLICT.code}: File exists: ${relPath}`
          );
        }

        // Update frontmatter with tags for Markdown files
        const resolvedPreset = resolveNotePreset({
          presetId: args.presetId,
          title:
            args.title?.trim() ||
            relPath
              .split("/")
              .pop()
              ?.replace(/\.[^.]+$/u, "") ||
            "Untitled",
          tags: normalizedTags,
          body: args.content,
        });
        let contentToWrite =
          resolvedPreset?.content ??
          args.content ??
          `# ${args.title?.trim() || "Untitled"}\n`;
        const isMarkdown =
          relPath.endsWith(".md") || relPath.endsWith(".markdown");
        if (isMarkdown && normalizedTags.length > 0) {
          // Use shared utility that handles all frontmatter edge cases
          contentToWrite = updateFrontmatterTags(
            contentToWrite,
            normalizedTags
          );
        }

        await mkdir(dirname(absPath), { recursive: true });
        await atomicWrite(absPath, contentToWrite);

        const results = await defaultSyncService.syncFiles(
          collection,
          ctx.store,
          [relPath],
          { runUpdateCmd: false, gitPull: false }
        );
        const syncResult = results[0];
        if (!syncResult) {
          throw new Error("RUNTIME: Sync result missing");
        }
        if (syncResult.status === "error") {
          throw new Error(
            `INGEST_ERROR: ${syncResult.errorCode ?? "ERROR"} - ${
              syncResult.errorMessage ?? "Unknown error"
            }`
          );
        }

        let docid = syncResult.docid;
        let documentId: number | undefined;
        if (!docid) {
          const docResult = await ctx.store.getDocument(
            collectionName,
            relPath
          );
          if (!docResult.ok) {
            throw new Error(docResult.error.message);
          }
          if (!docResult.value) {
            throw new Error("RUNTIME: Document missing after sync");
          }
          docid = docResult.value.docid;
          documentId = docResult.value.id;
        } else {
          // Get document id for tag storage
          const docResult = await ctx.store.getDocument(
            collectionName,
            relPath
          );
          if (docResult.ok && docResult.value) {
            documentId = docResult.value.id;
          }
        }

        // For non-markdown files, store tags as user-source in DB
        // (Markdown files get tags from frontmatter during sync)
        let tagsStored = isMarkdown; // Markdown tags stored via frontmatter sync
        if (!isMarkdown && normalizedTags.length > 0 && documentId) {
          const tagResult = await ctx.store.setDocTags(
            documentId,
            normalizedTags,
            "user"
          );
          if (tagResult.ok) {
            tagsStored = true;
          } else {
            // Log warning - document created but tags not stored
            console.error(
              `[MCP] Warning: Document created but tags not stored: ${tagResult.error.message}`
            );
          }
        }

        // Only include tags in response if confirmed stored
        const confirmedTags =
          tagsStored && normalizedTags.length > 0 ? normalizedTags : undefined;

        return {
          docid,
          uri: buildUri(collectionName, relPath),
          absPath,
          collection: collectionName,
          relPath,
          created: !exists,
          overwritten: exists,
          serverInstanceId: ctx.serverInstanceId,
          tags: confirmedTags,
        };
      });
    },
    formatCaptureResult
  );
}
