/**
 * ReadFileTool - Read the complete content of a file from the vault
 *
 * This is a read-only tool, so:
 * - isWriteOperation = false
 * - No approval needed
 * - No checkpoint needed
 */

import { TFile } from 'obsidian';
import { BaseTool } from '../BaseTool';
import type { ToolDefinition, ToolExecutionContext } from '../types';
import type ObsidianAgentPlugin from '../../../main';
import { getInternalAgentFolderPath } from '../../utils/agentFolder';

/**
 * Maximum characters to return. ~12500 tokens at 4 chars/token.
 * Aligned with Claude Code's read budget; covers a typical 60-90 min
 * meeting transcript or a long article without truncation.
 */
const MAX_CONTENT_CHARS = 50_000;

/**
 * BUG-020: Agents sometimes call `read_file("tmp/task-<id>/result.md")`
 * without the `<agent-folder>/` prefix that the externaliser actually
 * writes. When that happens the vault adapter misses and the tool reports
 * "not found". Detect the shortened form and retry against the full path.
 * Strict prefix check prevents unrelated `tmp.md` files from being
 * redirected, and `..` is rejected to stay on the safe side.
 *
 * Exported for unit tests; keep it pure so it doesn't need the plugin.
 */
export function looksLikeExternalisedTmpPath(path: string): boolean {
    if (!path.startsWith('tmp/task-')) return false;
    const segments = path.split('/');
    if (segments.some((s) => s === '..' || s.includes('\0'))) return false;
    // Must have at least tmp/task-<id>/<filename>
    return segments.length >= 3;
}

interface ReadFileInput {
    path: string;
}

export class ReadFileTool extends BaseTool<'read_file'> {
    readonly name = 'read_file' as const;
    readonly isWriteOperation = false;

    constructor(plugin: ObsidianAgentPlugin) {
        super(plugin);
    }

    getDefinition(): ToolDefinition {
        return {
            name: 'read_file',
            description:
                'Read the complete content of a file from the vault. Use this to view notes, check existing content before editing, or gather information.',
            input_schema: {
                type: 'object',
                properties: {
                    path: {
                        type: 'string',
                        description:
                            'Path to the file relative to vault root (e.g., "folder/note.md")',
                    },
                },
                required: ['path'],
            },
        };
    }

    async execute(input: Record<string, unknown>, context: ToolExecutionContext): Promise<void> {
        const { path } = input as unknown as ReadFileInput;
        const { callbacks } = context;

        try {
            // Validate input
            if (!path) {
                throw new Error('Path parameter is required');
            }

            // Get the file from vault (indexed files)
            const file = this.app.vault.getAbstractFileByPath(path);

            let content: string;
            let filePath: string;
            let basename: string;
            let extension: string;

            if (file && file instanceof TFile) {
                // Standard path: file is in Obsidian's vault index
                content = await this.app.vault.read(file);
                filePath = file.path;
                basename = file.basename;
                extension = file.extension;
            } else {
                // Fallback 1: file might be in a hidden/dot folder (e.g. .obsidian-agent/)
                // that Obsidian doesn't index. Use the adapter for direct filesystem access.
                let resolvedPath = path;
                let exists = await this.app.vault.adapter.exists(resolvedPath);

                // Fallback 2 (BUG-020): LLMs occasionally call
                // read_file("tmp/task-<id>/result.md") without the
                // agent-folder prefix that the externaliser actually wrote
                // (`<agent-folder>/tmp/task-<id>/result.md`). Retry against
                // the prefixed path when the short form matches the
                // externalisation pattern.
                if (!exists && looksLikeExternalisedTmpPath(path)) {
                    const prefixed = `${getInternalAgentFolderPath(this.plugin)}/${path}`;
                    if (await this.app.vault.adapter.exists(prefixed)) {
                        resolvedPath = prefixed;
                        exists = true;
                    }
                }

                if (!exists) {
                    callbacks.pushToolResult(
                        this.formatError(new Error(`File not found: ${path}`)),
                    );
                    return;
                }
                content = await this.app.vault.adapter.read(resolvedPath);
                filePath = resolvedPath;
                const parts = resolvedPath.split('/');
                const filename = parts[parts.length - 1] ?? resolvedPath;
                const dotIdx = filename.lastIndexOf('.');
                basename = dotIdx > 0 ? filename.substring(0, dotIdx) : filename;
                extension = dotIdx > 0 ? filename.substring(dotIdx + 1) : '';
            }

            // Truncate very large files to prevent context explosion
            const originalLength = content.length;
            if (content.length > MAX_CONTENT_CHARS) {
                content = content.slice(0, MAX_CONTENT_CHARS);
            }

            // Return formatted content
            const result = this.formatContent(content, {
                path: filePath,
                basename,
                extension,
            });

            if (originalLength > MAX_CONTENT_CHARS) {
                const overflowRatio = (originalLength - MAX_CONTENT_CHARS) / originalLength;
                const hint = overflowRatio > 0.1
                    ? ' Use search_files for specific content.'
                    : '';
                callbacks.pushToolResult(
                    result + `\n[Truncated: showing ${MAX_CONTENT_CHARS} of ${originalLength} chars.${hint}]`,
                );
            } else {
                callbacks.pushToolResult(result);
            }
            callbacks.log(`Successfully read file: ${path} (${content.length} chars)`);
        } catch (error) {
            callbacks.pushToolResult(this.formatError(error));
            await callbacks.handleError('read_file', error);
        }
    }
}
