import path from 'path';
import os from 'os';
import { resolveCaseInsensitivePath } from './actor.utils';
import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf';
import { DocxLoader } from '@langchain/community/document_loaders/fs/docx';
import { PPTXLoader } from '@langchain/community/document_loaders/fs/pptx';
import ExcelJS from 'exceljs';
import { getSensitivePathReason } from '../../security';
import {
    type ConfirmationContext,
    requestOperationConfirmation,
} from '../confirmation';
import { isPathDeniedForRead } from '../../sandbox';
import { createChildLogger } from '../../logger';

const log = createChildLogger({ component: 'read_file' });

/**
 * Arguments for the read_file tool.
 */
export interface ReadFileArgs {
    /** The file path to read (absolute or relative to home directory) */
    path: string;
    /** Starting line offset (0-based). For text files only. */
    offset?: number;
    /** Maximum number of lines to read. For text files only. */
    limit?: number;
}

export interface FileContentResult {
    query: string;
    file: string;
    uri: string;
    compiled: string | Array<{ type: string; [key: string]: any }>;
    isImage?: boolean;
}

/**
 * Options for file reading operations
 */
export interface ReadFileOptions {
    /** Confirmation context for requesting user approval on sensitive paths */
    confirmationContext?: ConfirmationContext;
}

/** Default maximum lines to read when no limit is specified */
const DEFAULT_LINE_LIMIT = 50;

/**
 * Sanitize text to remove problematic Unicode escape sequences that PostgreSQL JSON parser rejects.
 * This includes null bytes (\u0000) and other control characters that can appear in PDF text extraction.
 */
function sanitizeTextForJson(text: string): string {
    // Remove null bytes and other problematic control characters (U+0000 to U+001F except common whitespace)
    // Keep \t (0x09), \n (0x0A), \r (0x0D)
    return text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '');
}

// Supported image formats
const IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.webp'];

// Supported document formats
const PDF_EXTENSION = '.pdf';
const DOCX_EXTENSIONS = ['.docx', '.doc'];
const EXCEL_EXTENSIONS = ['.xlsx'];
const PPT_EXTENSIONS = ['.pptx', '.ppt'];

/**
 * Check if a file path is an image based on extension
 */
function isImageFile(filePath: string): boolean {
    const ext = path.extname(filePath).toLowerCase();
    return IMAGE_EXTENSIONS.includes(ext);
}

/**
 * Check if a file path is a PDF based on extension
 */
function isPdfFile(filePath: string): boolean {
    const ext = path.extname(filePath).toLowerCase();
    return ext === PDF_EXTENSION;
}

/**
 * Check if a file path is a Word document based on extension
 */
function isWordDoc(filePath: string): boolean {
    const ext = path.extname(filePath).toLowerCase();
    return DOCX_EXTENSIONS.includes(ext);
}

/**
 * Check if a file path is an Excel spreadsheet based on extension
 */
function isExcelFile(filePath: string): boolean {
    const ext = path.extname(filePath).toLowerCase();
    return EXCEL_EXTENSIONS.includes(ext);
}

/**
 * Check if a file path is a PowerPoint presentation based on extension
 */
function isPptFile(filePath: string): boolean {
    const ext = path.extname(filePath).toLowerCase();
    return PPT_EXTENSIONS.includes(ext);
}

/**
 * Get MIME type from file extension
 */
function getMimeType(filePath: string): string {
    const ext = path.extname(filePath).toLowerCase();
    const mimeTypes: Record<string, string> = {
        '.jpg': 'image/jpeg',
        '.jpeg': 'image/jpeg',
        '.png': 'image/png',
        '.webp': 'image/webp',
    };
    return mimeTypes[ext] || 'application/octet-stream';
}

/**
 * Result of applying line filtering to text content
 */
interface LineFilterResult {
    content: string;
    truncated: boolean;
    totalLines: number;
    startLine: number;
    endLine: number;
}

/**
 * Apply offset/limit filtering to lines of text.
 *
 * @param text - The raw text content
 * @param offset - Starting line offset (0-based), defaults to 0
 * @param limit - Maximum number of lines to read, defaults to DEFAULT_LINE_LIMIT
 * @returns Filtered content with metadata
 */
function applyLineFilter(text: string, offset: number = 0, limit?: number): LineFilterResult {
    const lines = text.split('\n');
    const totalLines = lines.length;

    // Clamp offset to valid range
    const startIdx = Math.max(0, Math.min(offset, totalLines));

    // Apply limit (default to DEFAULT_LINE_LIMIT if not specified)
    const effectiveLimit = limit ?? DEFAULT_LINE_LIMIT;
    const endIdx = Math.min(startIdx + effectiveLimit, totalLines);

    const selectedLines = lines.slice(startIdx, endIdx);
    const truncated = endIdx < totalLines;

    return {
        content: selectedLines.join('\n'),
        truncated,
        totalLines,
        startLine: startIdx,
        endLine: endIdx,
    };
}

/**
 * Format truncation message for output
 */
function formatTruncationMessage(result: LineFilterResult, fileType: string = 'File'): string {
    if (!result.truncated) return '';
    return `\n\n[${fileType} truncated: showing lines ${result.startLine + 1}-${result.endLine} of ${result.totalLines}. Use offset/limit parameters to view more.]`;
}

/**
 * View the contents of a file with optional line range specification.
 *
 * Supports:
 * - Text files with offset/limit filtering
 * - Images (jpg, jpeg, png, webp) - returned as base64
 * - PDFs - text extraction with offset/limit
 * - Word documents (.docx, .doc)
 * - Excel spreadsheets (.xlsx, .xls)
 * - PowerPoint presentations (.pptx, .ppt)
 *
 * Security:
 * - Sensitive paths (SSH keys, credentials, etc.) require user confirmation
 */
export async function readFile(
    args: ReadFileArgs,
    options?: ReadFileOptions
): Promise<FileContentResult> {
    const { path: filePath, offset = 0, limit } = args;

    let query = `View file: ${filePath}`;
    if (offset > 0 || limit) {
        const parts: string[] = [];
        if (offset > 0) parts.push(`offset=${offset}`);
        if (limit) parts.push(`limit=${limit}`);
        query += ` (${parts.join(', ')})`;
    }

    try {
        // Expand tilde to home directory if present
        const expandedPath = filePath.startsWith('~/')
            ? path.join(os.homedir(), filePath.slice(2))
            : filePath === '~'
                ? os.homedir()
                : filePath;

        // Resolve to absolute path (relative paths resolve relative to home folder)
        const absolutePath = path.isAbsolute(expandedPath)
            ? expandedPath
            : path.resolve(os.homedir(), expandedPath);

        // Check if path is in denied read paths (configurable, defaults to sensitive paths)
        // and request confirmation if needed
        if (isPathDeniedForRead(absolutePath) && options?.confirmationContext) {
            const reason = getSensitivePathReason(absolutePath) || 'protected file';
            const confirmResult = await requestOperationConfirmation(
                'read_sensitive_file',
                absolutePath,
                options.confirmationContext,
                {
                    toolName: 'view_file',
                    toolArgs: { path: filePath, offset, limit },
                    additionalMessage: `This path contains ${reason}.\n\nAre you sure you want to read this file?`,
                }
            );

            if (!confirmResult.approved) {
                return {
                    query,
                    file: filePath,
                    uri: filePath,
                    compiled: `File read cancelled: ${confirmResult.denialReason || 'User denied access to protected file'}`,
                };
            }
        }

        // Read the file using Bun.file
        let resolvedPath = absolutePath;
        let file = Bun.file(resolvedPath);
        let exists = await file.exists();

        // If the exact-cased path doesn't exist, try resolving case-insensitively.
        if (!exists) {
            const caseResolved = await resolveCaseInsensitivePath(path.normalize(absolutePath));
            if (caseResolved) {
                resolvedPath = caseResolved;
                file = Bun.file(resolvedPath);
                exists = await file.exists();
            }
        }

        if (!exists) {
            return {
                query,
                file: filePath,
                uri: filePath,
                compiled: `File '${filePath}' not found`,
            };
        }

        // Check if file is an image
        if (isImageFile(resolvedPath)) {
            try {
                log.debug(`[Image] Reading: ${resolvedPath}`);
                const arrayBuffer = await file.arrayBuffer();
                const base64 = Buffer.from(arrayBuffer).toString('base64');
                const mimeType = getMimeType(resolvedPath);
                log.debug(`[Image] Encoded: ${(arrayBuffer.byteLength / 1024).toFixed(2)} KB as ${mimeType}`);

                // Return multimodal content for vision-enabled models
                return {
                    query,
                    file: filePath,
                    uri: filePath,
                    compiled: [
                        {
                            type: 'text',
                            text: `Read image file: ${filePath}\nSize: ${(arrayBuffer.byteLength / 1024).toFixed(2)} KB\nFormat: ${mimeType}`
                        },
                        {
                            type: 'image',
                            source_type: 'base64',
                            mime_type: mimeType,
                            data: base64,
                        }
                    ],
                    isImage: true,
                };
            } catch (imageError) {
                log.error({ err: imageError }, `Error reading image ${filePath}`);
                return {
                    query,
                    file: filePath,
                    uri: filePath,
                    compiled: `Error reading image file ${filePath}: ${imageError instanceof Error ? imageError.message : String(imageError)}`,
                };
            }
        }

        // Check if file is a PDF
        if (isPdfFile(resolvedPath)) {
            try {
                log.debug(`[PDF] Reading: ${resolvedPath}`);
                const loader = new PDFLoader(resolvedPath, {
                    splitPages: false,
                });
                const docs = await loader.load();

                if (docs.length === 0) {
                    return {
                        query,
                        file: filePath,
                        uri: filePath,
                        compiled: `PDF file '${filePath}' contains no readable text content.`,
                    };
                }

                const rawPdfText = docs.map(doc => doc.pageContent).join('\n\n');
                // Sanitize to remove problematic Unicode characters that break PostgreSQL JSON storage
                const pdfText = sanitizeTextForJson(rawPdfText);
                const pageCount = (docs[0]?.metadata as any)?.pdf?.totalPages || docs.length;
                log.debug(`[PDF] Extracted ${pdfText.length} characters from ${pageCount} page(s)`);

                const filterResult = applyLineFilter(pdfText, offset, limit);
                const truncationMsg = formatTruncationMessage(filterResult, 'PDF');
                const filteredText = `[PDF: ${pageCount} page(s), ${filterResult.totalLines} lines]\n\n${filterResult.content}${truncationMsg}`;

                return {
                    query,
                    file: filePath,
                    uri: filePath,
                    compiled: filteredText,
                };
            } catch (pdfError) {
                log.error({ err: pdfError }, `Error reading PDF ${filePath}`);
                return {
                    query,
                    file: filePath,
                    uri: filePath,
                    compiled: `Error reading PDF file ${filePath}: ${pdfError instanceof Error ? pdfError.message : String(pdfError)}`,
                };
            }
        }

        // Check if file is a Word document
        if (isWordDoc(resolvedPath)) {
            try {
                log.debug(`[DOCX] Reading: ${resolvedPath}`);
                const loader = new DocxLoader(resolvedPath);
                const docs = await loader.load();

                if (docs.length === 0) {
                    return {
                        query,
                        file: filePath,
                        uri: filePath,
                        compiled: `Word document '${filePath}' contains no readable text content.`,
                    };
                }

                const rawDocText = docs.map(doc => doc.pageContent).join('\n\n');
                const docText = sanitizeTextForJson(rawDocText);
                log.debug(`[DOCX] Extracted ${docText.length} characters`);

                const filterResult = applyLineFilter(docText, offset, limit);
                const truncationMsg = formatTruncationMessage(filterResult, 'Document');
                const filteredText = `[Word Document: ${filterResult.totalLines} lines]\n\n${filterResult.content}${truncationMsg}`;

                return {
                    query,
                    file: filePath,
                    uri: filePath,
                    compiled: filteredText,
                };
            } catch (docxError) {
                log.error({ err: docxError }, `Error reading Word document ${filePath}`);
                return {
                    query,
                    file: filePath,
                    uri: filePath,
                    compiled: `Error reading Word document ${filePath}: ${docxError instanceof Error ? docxError.message : String(docxError)}`,
                };
            }
        }

        // Check if file is an Excel spreadsheet
        if (isExcelFile(resolvedPath)) {
            try {
                log.debug(`[XLSX] Reading: ${resolvedPath}`);
                const { Readable } = await import('stream');
                const nodeStream = Readable.fromWeb(file.stream() as any);
                const reader = new ExcelJS.stream.xlsx.WorkbookReader(nodeStream, {
                    sharedStrings: 'cache',
                    styles: 'ignore',
                    hyperlinks: 'ignore',
                    worksheets: 'emit',
                    entries: 'emit',
                });

                // Stream sheets and rows to build CSV text
                const sheetsText: string[] = [];
                let sheetCount = 0;
                for await (const worksheetReader of reader) {
                    sheetCount++;
                    const rows: string[] = [];
                    for await (const row of worksheetReader) {
                        const cells: string[] = [];
                        for (let c = 1; c <= row.cellCount; c++) {
                            const text = row.getCell(c).text ?? '';
                            if (text.includes(',') || text.includes('"') || text.includes('\n')) {
                                cells.push(`"${text.replace(/"/g, '""')}"`);
                            } else {
                                cells.push(text);
                            }
                        }
                        rows.push(cells.join(','));
                    }
                    sheetsText.push(`--- Sheet: ${(worksheetReader as any).name} ---\n${rows.join('\n')}`);
                }

                if (sheetCount === 0) {
                    return {
                        query,
                        file: filePath,
                        uri: filePath,
                        compiled: `Excel file '${filePath}' contains no sheets.`,
                    };
                }

                const xlsxText = sheetsText.join('\n\n');
                log.debug(`[XLSX] Extracted ${xlsxText.length} characters from ${sheetCount} sheet(s)`);

                const filterResult = applyLineFilter(xlsxText, offset, limit);
                const truncationMsg = formatTruncationMessage(filterResult, 'Spreadsheet');
                const filteredText = `[Excel: ${sheetCount} sheet(s), ${filterResult.totalLines} lines]\n\n${filterResult.content}${truncationMsg}`;

                return {
                    query,
                    file: filePath,
                    uri: filePath,
                    compiled: filteredText,
                };
            } catch (excelError) {
                log.error({ err: excelError }, `Error reading Excel file ${filePath}`);
                return {
                    query,
                    file: filePath,
                    uri: filePath,
                    compiled: `Error reading Excel file ${filePath}: ${excelError instanceof Error ? excelError.message : String(excelError)}`,
                };
            }
        }

        // Check if file is a PowerPoint presentation
        if (isPptFile(resolvedPath)) {
            try {
                log.debug(`[PPT] Reading: ${resolvedPath}`);
                const loader = new PPTXLoader(resolvedPath);
                const docs = await loader.load();

                if (docs.length === 0) {
                    return {
                        query,
                        file: filePath,
                        uri: filePath,
                        compiled: `PowerPoint file '${filePath}' contains no readable text content.`,
                    };
                }

                const rawPptText = docs.map(doc => doc.pageContent).join('\n\n');
                const pptText = sanitizeTextForJson(rawPptText);
                log.debug(`[PPT] Extracted ${pptText.length} characters`);

                const filterResult = applyLineFilter(pptText, offset, limit);
                const truncationMsg = formatTruncationMessage(filterResult, 'Presentation');
                const filteredText = `[PowerPoint: ${filterResult.totalLines} lines]\n\n${filterResult.content}${truncationMsg}`;

                return {
                    query,
                    file: filePath,
                    uri: filePath,
                    compiled: filteredText,
                };
            } catch (pptError) {
                log.error({ err: pptError }, `Error reading PowerPoint file ${filePath}`);
                return {
                    query,
                    file: filePath,
                    uri: filePath,
                    compiled: `Error reading PowerPoint file ${filePath}: ${pptError instanceof Error ? pptError.message : String(pptError)}`,
                };
            }
        }

        // Read file content as text (default handler)
        const rawText = await file.text();
        const filterResult = applyLineFilter(rawText, offset, limit);
        const truncationMsg = formatTruncationMessage(filterResult, 'File');
        const filteredText = filterResult.content + truncationMsg;

        return {
            query,
            file: filePath,
            uri: filePath,
            compiled: filteredText,
        };
    } catch (error) {
        const errorMsg = `Error reading file ${filePath}: ${error instanceof Error ? error.message : String(error)}`;
        log.error({ err: error }, errorMsg);

        return {
            query,
            file: filePath,
            uri: filePath,
            compiled: errorMsg,
        };
    }
}
