use crate::validate_path;
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
use kodegen_config::shorten_path_for_display;
use kodegen_mcp_schema::filesystem::{FsReadFileArgs, FsReadFileOutput, ReadFilePrompts};
use kodegen_mcp_schema::{Tool, ToolExecutionContext, ToolResponse, McpError};
use mime_guess::from_path;

use std::collections::VecDeque;
use std::path::Path;
use tokio::fs;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::time::{Duration, timeout};

// ============================================================================
// INTERNAL TYPES
// ============================================================================

/// Internal file read result (before conversion to MCP output)
/// 
/// This struct mirrors FsReadFileOutput but excludes fields that are
/// added by execute() (success, path). All internal methods return this
/// instead of serde_json::Value for compile-time type safety.
#[derive(Debug, Clone)]
struct InternalReadResult {
    /// File content (text or base64-encoded binary)
    content: String,
    
    /// MIME type detected from file extension or HTTP headers
    mime_type: String,
    
    /// Whether this is an image file (determines base64 encoding)
    is_image: bool,
    
    /// Size in bytes (Some for images/URLs, None for text files from disk)
    size_bytes: Option<u64>,
    
    /// Total lines in file (Some for text files from disk, None for images/URLs)
    total_lines: Option<u64>,
    
    /// Number of lines actually read (Some for text files from disk, None for images/URLs)
    lines_read: Option<u64>,
    
    /// Whether this is a partial read (offset != 0 or didn't read to EOF)
    is_partial: bool,
}

// ============================================================================
// HELPERS
// ============================================================================

fn is_image_mime(mt: &str) -> bool {
    mt.to_lowercase().starts_with("image/")
}

/// Read lines starting from offset, returning both lines and total count in ONE pass
///
/// This is more efficient than separate counting because we iterate once:
/// - Count all lines while processing
/// - Skip lines until we reach start position
/// - Collect lines until we reach start + count
///
/// Memory: O(count) instead of `O(total_lines)`
async fn read_lines_forward_with_total(
    path: &std::path::Path,
    start: usize,
    count: usize,
) -> Result<(Vec<String>, Option<usize>), McpError> {
    let file = tokio::fs::File::open(path).await?;

    let reader = BufReader::new(file);
    let mut lines_stream = reader.lines();

    let mut result = Vec::with_capacity(count);
    let mut line_number = 0;

    while let Some(line) = lines_stream.next_line().await? {
        // Collect lines in target range
        if line_number >= start && result.len() < count {
            result.push(line);
        }

        line_number += 1;
    }

    Ok((result, Some(line_number)))
}

/// Read last N lines using ring buffer, returning both lines and total count in ONE pass
///
/// Uses `VecDeque` as circular buffer:
/// - Capacity = `tail_count` (memory efficient)
/// - Pop oldest when full, push newest
/// - Result: last N lines + total line count
///
/// Memory: `O(tail_count)` instead of `O(total_lines)`
async fn read_lines_tail_with_total(
    path: &std::path::Path,
    tail_count: usize,
) -> Result<(Vec<String>, Option<usize>), McpError> {
    let file = tokio::fs::File::open(path).await?;

    let reader = BufReader::new(file);
    let mut lines_stream = reader.lines();

    // Handle tail_count = 0 edge case
    if tail_count == 0 {
        // Count total lines but don't collect any
        let mut total_lines = 0;
        while lines_stream.next_line().await?.is_some() {
            total_lines += 1;
        }
        return Ok((Vec::new(), Some(total_lines)));
    }

    // Ring buffer for last N lines
    let mut ring_buffer = VecDeque::with_capacity(tail_count);
    let mut total_lines = 0;

    while let Some(line) = lines_stream.next_line().await? {
        if ring_buffer.len() == tail_count {
            ring_buffer.pop_front(); // Remove oldest
        }
        ring_buffer.push_back(line);
        total_lines += 1;
    }

    Ok((ring_buffer.into_iter().collect(), Some(total_lines)))
}

// ============================================================================
// TOOL STRUCT
// ============================================================================

#[derive(Clone)]
pub struct ReadFileTool {
    default_line_limit: usize,
    config_manager: kodegen_config_manager::ConfigManager,
}

impl ReadFileTool {
    #[must_use]
    pub fn new(
        default_line_limit: usize,
        config_manager: kodegen_config_manager::ConfigManager,
    ) -> Self {
        Self {
            default_line_limit,
            config_manager,
        }
    }

    /// Read file from disk with full validation
    async fn read_file_from_disk(
        &self,
        path: &str,
        offset: i64,
        length: Option<usize>,
        client_pwd: Option<&Path>,
    ) -> Result<InternalReadResult, McpError> {
        let valid_path = validate_path(path, &self.config_manager, client_pwd).await?;

        let guessed = from_path(&valid_path)
            .first_or_octet_stream()
            .essence_str()
            .to_owned();

        let is_img = is_image_mime(&guessed);

        // If image, read as base64
        if is_img {
            let bytes = fs::read(&valid_path).await?;
            let base64_content = BASE64.encode(&bytes);

            return Ok(InternalReadResult {
                content: base64_content,
                mime_type: guessed,
                is_image: true,
                size_bytes: Some(bytes.len() as u64),
                total_lines: None,
                lines_read: None,
                is_partial: false,
            });
        }

        // Handle text files - use streaming to avoid loading entire file
        let (lines_vec, total) = if offset < 0 {
            // Tail behavior: read last N lines
            let tail_count = usize::try_from(-offset).unwrap_or(0);
            read_lines_tail_with_total(&valid_path, tail_count).await?
        } else {
            // Forward read: skip to offset, read length lines
            let start = usize::try_from(offset).unwrap_or(0);
            let count = length.unwrap_or(self.default_line_limit);
            read_lines_forward_with_total(&valid_path, start, count).await?
        };

        let lines_read = lines_vec.len();
        let truncated = lines_vec.join("\n");

        // Calculate start/end for notice formatting
        let (start, end) = if offset < 0 {
            // Tail reads always have total
            if let Some(t) = total {
                let start = t.saturating_sub(lines_read);
                (start, t)
            } else {
                // Fallback (should not happen for tail reads)
                (0, lines_read)
            }
        } else {
            let start = usize::try_from(offset).unwrap_or(0);
            let end = start + lines_read;
            (start, end)
        };

        let mut content = truncated;

        // Determine if this is a partial read
        let is_partial = offset != 0 || total.is_none_or(|t| end < t);

        // If partial read, add a notice
        if is_partial {
            let notice = if offset < 0 {
                // Tail reads always have total
                if let Some(t) = total {
                    format!(
                        "[Reading last {} lines (lines {}-{}) of {} total lines]\n\n",
                        end - start,
                        start,
                        end - 1,
                        t
                    )
                } else {
                    // Fallback (should not happen for tail reads)
                    format!("[Reading last {} lines]\n\n", end - start)
                }
            } else {
                // Forward read: may or may not have total
                match total {
                    Some(t) => format!(
                        "[Reading {} lines from line {} of {} total lines]\n\n",
                        end - start,
                        start,
                        t
                    ),
                    None => format!("[Reading {} lines from line {}]\n\n", end - start, start),
                }
            };
            content = format!("{notice}{content}");
        }

        Ok(InternalReadResult {
            content,
            mime_type: guessed,
            is_image: false,
            size_bytes: None,
            total_lines: total.map(|t| t as u64),
            lines_read: Some((end - start) as u64),
            is_partial,
        })
    }

    /// Read file from URL with timeout
    async fn read_file_from_url(&self, url: &str) -> Result<InternalReadResult, McpError> {
        const FETCH_TIMEOUT_MS: u64 = 30000;

        let fetch_operation = async {
            let resp = reqwest::get(url)
                .await
                .map_err(|e| McpError::Network(e.to_string()))?;

            if !resp.status().is_success() {
                return Err(McpError::Network(format!(
                    "HTTP error, status: {}",
                    resp.status()
                )));
            }

            let content_type = resp
                .headers()
                .get("content-type")
                .map_or("text/plain", |v| v.to_str().unwrap_or("text/plain"))
                .to_owned();

            let bytes = resp
                .bytes()
                .await
                .map_err(|e| McpError::Network(e.to_string()))?;

            let is_image = is_image_mime(&content_type);

            if is_image {
                let base64_content = BASE64.encode(&bytes);
                Ok(InternalReadResult {
                    content: base64_content,
                    mime_type: content_type,
                    is_image: true,
                    size_bytes: Some(bytes.len() as u64),
                    total_lines: None,
                    lines_read: None,
                    is_partial: false,
                })
            } else {
                let content = String::from_utf8_lossy(&bytes).to_string();
                Ok(InternalReadResult {
                    content,
                    mime_type: content_type,
                    is_image: false,
                    size_bytes: Some(bytes.len() as u64),
                    total_lines: None,
                    lines_read: None,
                    is_partial: false,
                })
            }
        };

        match timeout(Duration::from_millis(FETCH_TIMEOUT_MS), fetch_operation).await {
            Ok(result) => result,
            Err(_) => Err(McpError::Other(anyhow::anyhow!(
                "Fetch timed out after {FETCH_TIMEOUT_MS}ms: {url}"
            ))),
        }
    }
}

// ============================================================================
// TOOL IMPLEMENTATION
// ============================================================================

impl Tool for ReadFileTool {
    type Args = FsReadFileArgs;
    type Prompts = ReadFilePrompts;

    fn name() -> &'static str {
        kodegen_mcp_schema::filesystem::FS_READ_FILE
    }

    fn description() -> &'static str {
        "Read the contents of a file from the filesystem or a URL. Supports text files (returned as text) \
         and image files (returned as base64). Use offset and length parameters to read specific \
         portions of large files. Supports negative offsets for tail behavior (offset: -N reads last N lines). \
         When offset is negative, length is ignored. Automatically validates paths and handles symlinks."
    }

    fn read_only() -> bool {
        true
    }

    fn open_world() -> bool {
        true // Can read from URLs
    }

    async fn execute(&self, args: Self::Args, ctx: ToolExecutionContext) -> Result<ToolResponse<<Self::Args as kodegen_mcp_schema::ToolArgs>::Output>, McpError> {
        // Auto-detect URL if not specified
        let is_url =
            args.is_url || args.path.starts_with("http://") || args.path.starts_with("https://");

        // Get result from helper
        let result = if is_url {
            self.read_file_from_url(&args.path).await?
        } else {
            self.read_file_from_disk(&args.path, args.offset, args.length, ctx.pwd())
                .await?
        };

        // Direct struct field access - compile-time guaranteed
        let content = result.content;
        let mime_type = result.mime_type;
        let is_image = result.is_image;
        let size_bytes = result.size_bytes;
        let total_lines = result.total_lines;
        let lines_read = result.lines_read;
        let is_partial = result.is_partial;

        // For URLs, display as-is; for file paths, use relative path if in git repo
        let display_path = if is_url {
            args.path.clone()
        } else {
            shorten_path_for_display(
                std::path::Path::new(&args.path),
                ctx.git_root()
            )
        };
        
        let summary = if is_image {
            let size_kb = size_bytes.map_or(0.0, |b| b as f64 / 1024.0);
            format!(
                "\x1b[36mRead image: {display_path}\x1b[0m\nFormat: {mime_type} · Size: {size_kb:.1} KB"
            )
        } else {
            let read = lines_read.unwrap_or(0);
            format!(
                "\x1b[36mRead file: {display_path}\x1b[0m\nContent: {read} lines · {} bytes · Use Content[1] for data",
                content.len()
            )
        };

        // For read_file, display includes the content itself
        let display = format!("{summary}\n\n{content}");

        Ok(ToolResponse::new(display, FsReadFileOutput {
            success: true,
            path: args.path,
            mime_type,
            is_image,
            size_bytes,
            total_lines,
            lines_read,
            is_partial,
            content,
        }))
    }
}
