use std::collections::HashSet;
use std::path::PathBuf;

use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use uuid::Uuid;

use second_brain_core::schema::{Conversation, Memory, MemoryType};

use crate::{Adapter, ConversationSource, ExtractedEntity, IngestResult};

pub struct ClaudeCodeAdapter {
    base_path: PathBuf,
    machine_id: String,
}

impl ClaudeCodeAdapter {
    pub fn new(machine_id: String) -> Self {
        let base_path = dirs_home().join(".claude");
        Self {
            base_path,
            machine_id,
        }
    }

    pub fn with_path(base_path: PathBuf, machine_id: String) -> Self {
        Self {
            base_path,
            machine_id,
        }
    }

    fn parse_conversation_file(
        &self,
        path: &PathBuf,
    ) -> Result<(
        Vec<Memory>,
        Vec<(ExtractedEntity, Uuid)>,
        Option<DateTime<Utc>>,
    )> {
        let content = std::fs::read_to_string(path)
            .with_context(|| format!("reading conversation file: {}", path.display()))?;

        let mut memories = Vec::new();
        let mut all_entities: Vec<(ExtractedEntity, Uuid)> = Vec::new();
        let mut seen_entities: HashSet<String> = HashSet::new();
        let mut first_timestamp: Option<DateTime<Utc>> = None;
        let source_id = path.to_string_lossy().to_string();

        for line in content.lines() {
            if line.trim().is_empty() {
                continue;
            }

            let msg: serde_json::Value = match serde_json::from_str(line) {
                Ok(v) => v,
                Err(_) => continue,
            };

            let msg_type = msg.get("type").and_then(|t| t.as_str()).unwrap_or("");

            if first_timestamp.is_none() {
                if let Some(ts) = msg.get("timestamp").and_then(|t| t.as_str()) {
                    first_timestamp = DateTime::parse_from_rfc3339(ts)
                        .map(|dt| dt.with_timezone(&Utc))
                        .ok();
                }
            }

            if msg_type != "assistant" {
                continue;
            }

            let message = match msg.get("message") {
                Some(m) => m,
                None => continue,
            };

            let content_blocks = match message.get("content").and_then(|c| c.as_array()) {
                Some(blocks) => blocks,
                None => continue,
            };

            for block in content_blocks {
                let block_type = block.get("type").and_then(|t| t.as_str()).unwrap_or("");

                let text = match block_type {
                    "text" => block.get("text").and_then(|t| t.as_str()),
                    _ => None,
                };

                if let Some(text) = text {
                    let chunks = chunk_text(text);
                    for chunk in chunks {
                        if !is_memorable(&chunk) {
                            continue;
                        }
                        let memory_type = classify_memory(&chunk);
                        let memory = Memory::new(
                            chunk.clone(),
                            memory_type,
                            "claude-code".to_string(),
                            source_id.clone(),
                        );
                        let memory_id = memory.id;
                        memories.push(memory);

                        for entity in extract_entities(&chunk) {
                            let key = entity.name.to_lowercase();
                            if seen_entities.insert(key) {
                                all_entities.push((entity, memory_id));
                            }
                        }
                    }
                }
            }
        }

        Ok((memories, all_entities, first_timestamp))
    }
}

impl Adapter for ClaudeCodeAdapter {
    fn name(&self) -> &str {
        "claude-code"
    }

    fn discover_conversations(&self) -> Result<Vec<ConversationSource>> {
        let projects_dir = self.base_path.join("projects");
        let mut sources = Vec::new();

        if !projects_dir.exists() {
            return Ok(sources);
        }

        let mut dirs = vec![projects_dir];
        while let Some(dir) = dirs.pop() {
            for entry in std::fs::read_dir(&dir)? {
                let entry = entry?;
                let path = entry.path();
                if path.is_dir() {
                    dirs.push(path);
                } else if path.extension().is_some_and(|e| e == "jsonl") {
                    sources.push(ConversationSource {
                        id: path.to_string_lossy().to_string(),
                        path: path.to_string_lossy().to_string(),
                        source_name: "claude-code".to_string(),
                        machine_id: self.machine_id.clone(),
                    });
                }
            }
        }

        Ok(sources)
    }

    fn ingest_conversation(&self, source: &ConversationSource) -> Result<IngestResult> {
        let path = PathBuf::from(&source.path);
        let (memories, entities, started_at) = self.parse_conversation_file(&path)?;

        let project_dir = path
            .parent()
            .and_then(|p| p.file_name().map(|n| n.to_string_lossy().to_string()));

        let conversation = Conversation {
            id: Uuid::new_v4(),
            source: "claude-code".to_string(),
            machine_id: source.machine_id.clone(),
            started_at: started_at.unwrap_or_else(Utc::now),
            project_path: project_dir,
        };

        let entity_count = entities.len();

        Ok(IngestResult {
            conversation,
            memories,
            entities,
            entities_extracted: entity_count,
        })
    }
}

fn dirs_home() -> PathBuf {
    std::env::var("HOME")
        .map(PathBuf::from)
        .unwrap_or_else(|_| PathBuf::from("/tmp"))
}

fn chunk_text(text: &str) -> Vec<String> {
    let mut chunks = Vec::new();
    let mut current = String::new();

    for paragraph in text.split("\n\n") {
        let trimmed = paragraph.trim();
        if trimmed.is_empty() {
            continue;
        }

        if current.len() + trimmed.len() > 1500 && !current.is_empty() {
            chunks.push(std::mem::take(&mut current));
        }

        if !current.is_empty() {
            current.push_str("\n\n");
        }
        current.push_str(trimmed);

        if current.len() >= 800 {
            chunks.push(std::mem::take(&mut current));
        }
    }

    if !current.is_empty() {
        chunks.push(current);
    }

    if chunks.is_empty() && !text.trim().is_empty() {
        chunks.push(text.trim().to_string());
    }

    chunks
}

fn is_memorable(text: &str) -> bool {
    if text.len() < 50 {
        return false;
    }

    let code_block_count = text.matches("```").count() / 2;
    if code_block_count > 8 {
        return false;
    }

    let non_code_lines = text
        .lines()
        .filter(|l| {
            let trimmed = l.trim();
            !trimmed.is_empty() && !trimmed.starts_with("```") && !trimmed.starts_with('\t')
        })
        .count();
    non_code_lines >= 1
}

fn classify_memory(text: &str) -> MemoryType {
    let lower = text.to_lowercase();
    let decision_markers = [
        "decided",
        "chose",
        "went with",
        "picked",
        "agreed",
        "choosing",
        "selected",
        "switched to",
        "migrated",
        "deployed",
        "shipped",
        "found that",
        "discovered",
        "realized",
        "confirmed",
        "concluded",
    ];
    let procedural_markers = [
        "to do this",
        "steps:",
        "step 1",
        "first,",
        "then,",
        "finally,",
        "run ",
        "install ",
        "execute",
        "command:",
        "configure",
        "set up",
        "to fix",
        "solution:",
        "you need to",
        "you can ",
        "you should",
        "here's how",
        "the fix",
    ];

    if decision_markers.iter().any(|m| lower.contains(m)) {
        MemoryType::Episodic
    } else if procedural_markers.iter().any(|m| lower.contains(m)) {
        MemoryType::Procedural
    } else {
        MemoryType::Semantic
    }
}

const TECH_NAMES: &[&str] = &[
    "rust",
    "python",
    "javascript",
    "typescript",
    "go",
    "java",
    "c++",
    "ruby",
    "swift",
    "kotlin",
    "react",
    "vue",
    "angular",
    "svelte",
    "next.js",
    "nuxt",
    "django",
    "flask",
    "fastapi",
    "express",
    "axum",
    "actix",
    "tokio",
    "serde",
    "docker",
    "kubernetes",
    "k8s",
    "terraform",
    "ansible",
    "nginx",
    "apache",
    "redis",
    "postgres",
    "postgresql",
    "mysql",
    "sqlite",
    "mongodb",
    "kuzudb",
    "neo4j",
    "elasticsearch",
    "kafka",
    "rabbitmq",
    "nats",
    "grpc",
    "graphql",
    "rest",
    "websocket",
    "ssh",
    "tailscale",
    "wireguard",
    "git",
    "github",
    "gitlab",
    "linux",
    "ubuntu",
    "debian",
    "macos",
    "windows",
    "arm64",
    "aarch64",
    "x86",
    "onnx",
    "pytorch",
    "tensorflow",
    "huggingface",
    "openai",
    "anthropic",
    "claude",
    "gpt",
    "llm",
    "mcp",
    "lsp",
    "systemd",
    "cron",
    "tmux",
    "vim",
    "neovim",
    "cargo",
    "npm",
    "pip",
    "uv",
    "bun",
    "deno",
    "node.js",
    "node",
    "aws",
    "gcp",
    "azure",
    "s3",
    "ec2",
    "lambda",
    "cloudflare",
    "raspberry pi",
    "pi",
    "jetson",
    "esp32",
    "arduino",
    "toml",
    "yaml",
    "json",
    "jsonl",
    "csv",
    "protobuf",
    "msgpack",
];

fn extract_entities(text: &str) -> Vec<ExtractedEntity> {
    let mut entities = Vec::new();
    let mut seen = HashSet::new();
    let lower = text.to_lowercase();

    for &tech in TECH_NAMES {
        if lower.contains(tech) {
            let key = tech.to_lowercase();
            if seen.insert(key) {
                entities.push(ExtractedEntity {
                    name: tech.to_string(),
                    entity_type: "technology".to_string(),
                });
            }
        }
    }

    for word in text.split_whitespace() {
        let clean = word.trim_matches(|c: char| {
            !c.is_alphanumeric() && c != '/' && c != '.' && c != '-' && c != '_'
        });
        if clean.contains('/') && clean.contains('.') {
            let ext_match = clean.rsplit('.').next().unwrap_or("");
            let is_source_file = matches!(
                ext_match,
                "rs" | "py"
                    | "ts"
                    | "tsx"
                    | "js"
                    | "jsx"
                    | "go"
                    | "java"
                    | "rb"
                    | "sh"
                    | "toml"
                    | "json"
                    | "yaml"
                    | "yml"
                    | "md"
                    | "sql"
                    | "html"
                    | "css"
            );
            if is_source_file {
                let key = clean.to_lowercase();
                if seen.insert(key) {
                    entities.push(ExtractedEntity {
                        name: clean.to_string(),
                        entity_type: "file_path".to_string(),
                    });
                }
            }
        }
    }

    entities
}
