use crate::{
    invocation::{SkillContext, SkillInput},
    outcome::SkillOutcome,
    skill::{Skill, SkillId},
};
use serde_json::json;
use std::collections::HashMap;

/// Forecast: predict which files the agent is likely to need next, based on access patterns + dep graph.
pub struct ForecastSkill;

impl Skill for ForecastSkill {
    fn id(&self) -> SkillId {
        SkillId("forecast")
    }
    fn display_name(&self) -> &str {
        "Forecast"
    }

    fn input_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "recently_accessed": {
                    "type": "array",
                    "items": { "type": "string" },
                    "description": "Files recently read or modified in this session"
                },
                "root": { "type": "string", "default": ".", "description": "Project root for dep scanning" },
                "top_k": { "type": "integer", "default": 5 }
            },
            "required": ["recently_accessed"]
        })
    }

    fn output_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "predicted": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "path": { "type": "string" },
                            "score": { "type": "number" },
                            "reason": { "type": "string" }
                        }
                    }
                }
            }
        })
    }

    fn execute(&self, input: SkillInput, _ctx: &SkillContext) -> anyhow::Result<SkillOutcome> {
        let recently_accessed = input.get_str_array("recently_accessed").unwrap_or_default();
        let root = input.get_str("root").unwrap_or(".").to_string();
        let top_k = input.get_u64("top_k").unwrap_or(5) as usize;

        let mut scores: HashMap<String, (f64, String)> = HashMap::new();

        // Score files that import from or are imported by recently_accessed files
        for accessed in &recently_accessed {
            let Ok(content) = std::fs::read_to_string(accessed) else {
                continue;
            };
            // Extract what this file imports
            for line in content.lines() {
                let t = line.trim();
                let imported = extract_import_path(t);
                if let Some(candidate) = imported {
                    let resolved = resolve_path(&root, accessed, &candidate);
                    if let Some(p) = resolved {
                        if !recently_accessed.contains(&p) {
                            let entry = scores.entry(p).or_insert((0.0, String::new()));
                            entry.0 += 1.0;
                            entry.1 = format!("imported by {accessed}");
                        }
                    }
                }
            }
        }

        let mut ranked: Vec<(String, f64, String)> =
            scores.into_iter().map(|(p, (s, r))| (p, s, r)).collect();
        ranked.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
        ranked.truncate(top_k);

        let predicted: Vec<serde_json::Value> = ranked
            .into_iter()
            .map(|(path, score, reason)| json!({ "path": path, "score": score, "reason": reason }))
            .collect();

        Ok(SkillOutcome::success(
            "forecast",
            json!({ "predicted": predicted }),
        ))
    }
}

pub(crate) fn extract_import_path(line: &str) -> Option<String> {
    if let Some(rest) = line.strip_prefix("use ") {
        return Some(rest.trim_end_matches(';').replace("::", "/"));
    }
    if line.starts_with("import ") {
        if let Some(from_idx) = line.rfind(" from ") {
            let rest = line[from_idx + 6..]
                .trim()
                .trim_end_matches(';')
                .trim()
                .trim_matches('"')
                .trim_matches('\'');
            if rest.starts_with('.') {
                return Some(rest.to_string());
            }
        }
    }
    None
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn rust_use_statement_converts_colons_to_slashes() {
        let result = extract_import_path("use std::collections::HashMap;");
        assert_eq!(result, Some("std/collections/HashMap".to_string()));
    }

    #[test]
    fn ts_relative_import_extracted() {
        let result = extract_import_path("import { foo } from './utils';");
        assert_eq!(result, Some("./utils".to_string()));
    }

    #[test]
    fn ts_relative_import_double_quote() {
        let result = extract_import_path("import { bar } from \"../lib\";");
        assert_eq!(result, Some("../lib".to_string()));
    }

    #[test]
    fn ts_absolute_npm_package_skipped() {
        assert_eq!(extract_import_path("import React from 'react';"), None);
    }

    #[test]
    fn plain_code_line_returns_none() {
        assert_eq!(extract_import_path("let x = 1;"), None);
        assert_eq!(extract_import_path("const y = foo();"), None);
    }

    #[test]
    fn use_with_crate_prefix() {
        let result = extract_import_path("use crate::skills::sieve;");
        assert_eq!(result, Some("crate/skills/sieve".to_string()));
    }
}

pub(crate) fn resolve_path(root: &str, current_file: &str, import: &str) -> Option<String> {
    let parent = std::path::Path::new(current_file).parent()?;
    let candidate = parent.join(import);
    // Try common extensions
    for ext in &["rs", "ts", "tsx", "js", "jsx", "py", "go"] {
        let with_ext = candidate.with_extension(ext);
        if with_ext.exists() {
            return Some(with_ext.to_string_lossy().to_string());
        }
        // Also try root-relative
        let from_root = std::path::Path::new(root).join(import).with_extension(ext);
        if from_root.exists() {
            return Some(from_root.to_string_lossy().to_string());
        }
    }
    None
}
