use std::path::PathBuf;
use std::sync::Arc;

use zeroclaw::agent::dispatcher::NativeToolDispatcher;
use zeroclaw::agent::Agent;
use zeroclaw::config::{Config as ZeroClawConfig, SkillsPromptInjectionMode};
use zeroclaw::memory::Memory;
use zeroclaw::observability::{NoopObserver, Observer};
use zeroclaw::providers::Provider;
use zeroclaw::runtime::NativeRuntime;
use zeroclaw::tools::{self, ReadSkillTool};
use zeroclaw::SecurityPolicy;

use crate::compat::memory_adapter::build_memory;
use crate::pipe::PipeError;
use crate::runtime::{RuntimeProfile, ToolPolicy};

const BROWSER_ACTION_TOOL_NAME: &str = "browser_action";
const SUPERRPA_BROWSER_TOOL_NAME: &str = "superrpa_browser";
const READ_SKILL_TOOL_NAME: &str = "read_skill";
const OPENXML_OFFICE_TOOL_NAME: &str = "openxml_office";
const SCREEN_HTML_EXPORT_TOOL_NAME: &str = "screen_html_export";
const BROWSER_TOOL_CONTRACT_PROMPT: &str = "SuperRPA browser interface contract:\n- Use superrpa_browser as the preferred dedicated SuperRPA interface inside this browser host.\n- browser_action is a legacy alias with the same contract; prefer superrpa_browser when choosing between them.\n- Browser actions allowed by policy are already approved by the user inside this BrowserAttached host.\n- Do not claim a browser action was denied, blocked, or rejected unless an actual tool call returns an error.\n- expected_domain must be the bare hostname only, for example www.zhihu.com.\n- Never include scheme, path, query, fragment, or port in expected_domain.\n- selector values are executed with document.querySelector(...), so they must be valid CSS selectors only.\n- Never use XPath selectors or jQuery-style :contains().\n- Prefer direct navigation to canonical URLs when they are known, instead of clicking text links to reach common pages.\n- If you need broad page content, use getText with a valid CSS selector such as body or a stable container.\n- If a task matches an installed skill, load that skill first and then execute it through the SuperRPA interface.";
const ZHIHU_HOTLIST_EXECUTION_PROMPT: &str = "Zhihu hotlist execution contract:\n- Treat Zhihu hotlist export/presentation requests as a real browser workflow, not as a text-only summarization task.\n- You must attempt the browser workflow before concluding failure; a prose-only answer is invalid for this workflow.\n- If the current page is not already `https://www.zhihu.com/hot`, navigate there first.\n- If the `zhihu-hotlist.extract_hotlist` skill tool is available, call it before any generic browser probing.\n- Use generic `getText` only as a last-resort fallback when the packaged extractor fails.\n- Extract ordered rows containing `rank`, `title`, and `heat` as structured data.\n- Do not use shell, web_fetch, web_search_tool, or fabricated sample data for this workflow.\n- Do not repeat the same sentence or section in your final answer.";
const OFFICE_EXPORT_COMPLETION_PROMPT: &str = "Export completion contract:\n- This task requires a real Excel export.\n- After the Zhihu rows are available, you must call openxml_office before finishing.\n- Never fabricate, simulate, or invent substitute hotlist data when a live collection/export task fails.\n- If live collection fails, report the failure concisely instead of producing fake rows.\n- Do not stop after describing how you will parse or export the data.\n- Do not repeat the same sentence or section in your final answer.\n- Your final answer must include the generated local .xlsx path.";
const SCREEN_EXPORT_COMPLETION_PROMPT: &str = "Presentation completion contract:\n- This task requires a real dashboard artifact.\n- After the Zhihu rows are available, you must call screen_html_export before finishing.\n- Do not stop after describing how you will render or present the data.\n- Do not repeat the same sentence or section in your final answer.\n- Your final answer must include the local .html path and the presentation object.";
const ZHIHU_WRITE_PUBLISH_PROMPT: &str = "Zhihu article publish contract:\n- This task may publish a Zhihu article.\n- You must not click publish without explicit human confirmation in the current session.\n- If the user asked to publish but no explicit confirmation phrase is present yet, ask for confirmation concisely and stop after the confirmation request.\n- Do not keep exploring tools after you have determined that publish confirmation is missing.\n- If the user only asked to write or draft, stay in draft mode and do not treat it as publish mode.\n- Do not repeat the same sentence or section in your final answer.";

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RuntimeEngine {
    profile: RuntimeProfile,
    tool_policy: ToolPolicy,
}

impl RuntimeEngine {
    pub fn new(profile: RuntimeProfile) -> Self {
        Self {
            profile,
            tool_policy: ToolPolicy::for_profile(profile),
        }
    }

    pub fn profile(&self) -> RuntimeProfile {
        self.profile
    }

    pub fn tool_policy(&self) -> &ToolPolicy {
        &self.tool_policy
    }

    pub fn browser_surface_enabled(&self) -> bool {
        self.tool_policy
            .allowed_tools
            .iter()
            .any(|tool| tool == BROWSER_ACTION_TOOL_NAME || tool == SUPERRPA_BROWSER_TOOL_NAME)
    }

    pub fn build_agent(
        &self,
        provider: Box<dyn Provider>,
        config: &ZeroClawConfig,
        skills_dirs: &[PathBuf],
        mut tools: Vec<Box<dyn zeroclaw::tools::Tool>>,
        browser_surface_present: bool,
        instruction: &str,
    ) -> Result<Agent, PipeError> {
        let memory: Arc<dyn Memory> =
            Arc::from(build_memory(config).map_err(map_anyhow_to_pipe_error)?);
        let security = Arc::new(SecurityPolicy::from_config(
            &config.autonomy,
            &config.workspace_dir,
        ));
        let observer: Arc<dyn Observer> = Arc::new(NoopObserver);
        let skills = self.load_skills_for_surface(config, skills_dirs, browser_surface_present);
        let (mut runtime_tools, _, _, _, _, _) = tools::all_tools_with_runtime(
            Arc::new(config.clone()),
            &security,
            Arc::new(NativeRuntime::new()),
            memory.clone(),
            None,
            None,
            &config.browser,
            &config.http_request,
            &config.web_fetch,
            &config.workspace_dir,
            &config.agents,
            config.api_key.as_deref(),
            config,
            None,
        );
        runtime_tools.append(&mut tools);

        let default_skills_dir = config.workspace_dir.join("skills");
        let has_custom_skills_dir = skills_dirs.iter().any(|d| *d != default_skills_dir);
        if matches!(
            config.skills.prompt_injection_mode,
            SkillsPromptInjectionMode::Compact
        ) && has_custom_skills_dir
        {
            let first_custom = skills_dirs
                .iter()
                .find(|d| **d != default_skills_dir)
                .cloned();
            runtime_tools.retain(|tool| tool.name() != READ_SKILL_TOOL_NAME);
            runtime_tools.push(Box::new(ReadSkillTool::with_runtime_skills_dir(
                config.workspace_dir.clone(),
                first_custom,
                config.skills.allow_scripts,
                config.skills.open_skills_enabled,
                config.skills.open_skills_dir.clone(),
            )));
        }

        Agent::builder()
            .provider(provider)
            .tools(runtime_tools)
            .memory(memory)
            .observer(observer)
            .tool_dispatcher(Box::new(NativeToolDispatcher))
            .config(config.agent.clone())
            .model_name(
                config
                    .default_model
                    .clone()
                    .unwrap_or_else(|| "deepseek-chat".to_string()),
            )
            .temperature(config.default_temperature)
            .workspace_dir(config.workspace_dir.clone())
            .skills(skills)
            .skills_prompt_mode(config.skills.prompt_injection_mode)
            .allowed_tools(self.allowed_tools_for_config(
                config,
                skills_dirs,
                browser_surface_present,
                instruction,
            ))
            .build()
            .map_err(map_anyhow_to_pipe_error)
    }

    pub fn build_instruction(
        &self,
        instruction: &str,
        page_url: Option<&str>,
        page_title: Option<&str>,
        browser_surface_present: bool,
    ) -> String {
        let trimmed_instruction = instruction.trim();
        if !browser_surface_present || !self.browser_surface_enabled() {
            return trimmed_instruction.to_string();
        }

        let mut sections = vec![BROWSER_TOOL_CONTRACT_PROMPT.to_string()];
        if is_zhihu_hotlist_task(trimmed_instruction, page_url, page_title) {
            sections.push(ZHIHU_HOTLIST_EXECUTION_PROMPT.to_string());
        }
        if task_needs_office_export(trimmed_instruction) {
            sections.push(OFFICE_EXPORT_COMPLETION_PROMPT.to_string());
        }
        if task_needs_screen_export(trimmed_instruction) {
            sections.push(SCREEN_EXPORT_COMPLETION_PROMPT.to_string());
        }
        if task_requests_zhihu_article_publish(trimmed_instruction, page_url, page_title) {
            sections.push(ZHIHU_WRITE_PUBLISH_PROMPT.to_string());
        }
        if let Some(page_context) = build_page_context_message(page_url, page_title) {
            sections.push(page_context);
        }
        sections.push(format!("User task: {trimmed_instruction}"));
        sections.join("\n\n")
    }

    pub fn loaded_skills(
        &self,
        config: &ZeroClawConfig,
        skills_dirs: &[PathBuf],
    ) -> Vec<zeroclaw::skills::Skill> {
        self.load_skills_for_surface(config, skills_dirs, self.browser_surface_enabled())
    }

    pub fn should_attach_openxml_office_tool(&self, instruction: &str) -> bool {
        task_needs_office_export(instruction)
    }

    pub fn should_attach_screen_html_export_tool(&self, instruction: &str) -> bool {
        task_needs_screen_export(instruction)
    }

    fn allowed_tools_for_config(
        &self,
        config: &ZeroClawConfig,
        skills_dirs: &[PathBuf],
        browser_surface_present: bool,
        instruction: &str,
    ) -> Option<Vec<String>> {
        let mut allowed_tools = self.tool_policy.allowed_tools.clone();
        let skills = self.load_skills_for_surface(config, skills_dirs, browser_surface_present);
        if !browser_surface_present {
            allowed_tools.retain(|tool| {
                tool != BROWSER_ACTION_TOOL_NAME && tool != SUPERRPA_BROWSER_TOOL_NAME
            });
        }
        if matches!(
            config.skills.prompt_injection_mode,
            SkillsPromptInjectionMode::Compact
        ) {
            allowed_tools.push(READ_SKILL_TOOL_NAME.to_string());
        }
        if task_needs_office_export(instruction) {
            allowed_tools.push(OPENXML_OFFICE_TOOL_NAME.to_string());
        }
        if task_needs_screen_export(instruction) {
            allowed_tools.push(SCREEN_HTML_EXPORT_TOOL_NAME.to_string());
        }
        if task_needs_local_file_read(instruction) {
            allowed_tools.push("file_read".to_string());
        }
        if browser_surface_present {
            allowed_tools.extend(browser_script_tool_names(&skills));
        }
        allowed_tools.dedup();

        if matches!(self.profile, RuntimeProfile::GeneralAssistant)
            && self.tool_policy.may_use_non_browser_tools
        {
            None
        } else {
            Some(allowed_tools)
        }
    }

    fn load_skills_for_surface(
        &self,
        config: &ZeroClawConfig,
        skills_dirs: &[PathBuf],
        browser_surface_present: bool,
    ) -> Vec<zeroclaw::skills::Skill> {
        let mut skills = load_runtime_skills(config, skills_dirs);
        if !browser_surface_present {
            skills.iter_mut().for_each(|skill| {
                skill.tools.retain(|tool| tool.kind != "browser_script");
            });
            skills.retain(|skill| !skill.tools.is_empty());
        }
        skills.sort_by(|left, right| {
            left.name
                .cmp(&right.name)
                .then(left.version.cmp(&right.version))
        });
        skills.dedup_by(|left, right| left.name == right.name && left.version == right.version);
        skills
    }
}

fn browser_script_tool_names(skills: &[zeroclaw::skills::Skill]) -> Vec<String> {
    skills
        .iter()
        .flat_map(|skill| {
            skill
                .tools
                .iter()
                .filter(|tool| tool.kind == "browser_script")
                .map(|tool| format!("{}.{}", skill.name, tool.name))
                .collect::<Vec<_>>()
        })
        .collect()
}

fn task_needs_local_file_read(instruction: &str) -> bool {
    let normalized = instruction.trim();
    normalized.contains("/home/") || normalized.contains("./") || normalized.contains("../")
}

pub fn is_zhihu_hotlist_task(
    instruction: &str,
    page_url: Option<&str>,
    page_title: Option<&str>,
) -> bool {
    let normalized_instruction = instruction.to_ascii_lowercase();
    let normalized_url = page_url.unwrap_or_default().to_ascii_lowercase();
    let normalized_title = page_title.unwrap_or_default().to_ascii_lowercase();

    let is_zhihu = normalized_instruction.contains("zhihu")
        || instruction.contains("知乎")
        || normalized_url.contains("zhihu.com")
        || normalized_title.contains("zhihu")
        || page_title.unwrap_or_default().contains("知乎");
    let hotlist_in_instruction = normalized_instruction.contains("hotlist")
        || instruction.contains("热榜");
    let hotlist_in_context = normalized_url.contains("/hot")
        || normalized_title.contains("hotlist")
        || page_title.unwrap_or_default().contains("热榜");

    // "热榜"/"hotlist" directly in the instruction implies Zhihu (the only
    // hotlist feature sgClaw supports).  Context-only signals (URL/title)
    // still require the "知乎" qualifier to avoid false positives.
    (is_zhihu && (hotlist_in_instruction || hotlist_in_context)) || hotlist_in_instruction
}

fn task_needs_office_export(instruction: &str) -> bool {
    let normalized = instruction.to_ascii_lowercase();
    normalized.contains("excel")
        || normalized.contains(".xlsx")
        || normalized.contains("导出")
        || normalized.contains("xlsx")
}

fn task_needs_screen_export(instruction: &str) -> bool {
    let normalized = instruction.to_ascii_lowercase();
    normalized.contains("大屏")
        || normalized.contains("看板")
        || normalized.contains("dashboard")
        || normalized.contains("screen")
        || normalized.contains("echarts")
        || normalized.contains("演示")
        || normalized.contains("汇报")
}

pub fn task_requests_zhihu_article_publish(
    instruction: &str,
    page_url: Option<&str>,
    page_title: Option<&str>,
) -> bool {
    if !is_zhihu_write_task(instruction, page_url, page_title) {
        return false;
    }

    let normalized = instruction.to_ascii_lowercase();
    normalized.contains("publish") || instruction.contains("发布") || instruction.contains("发表")
}

pub fn is_zhihu_write_task(
    instruction: &str,
    page_url: Option<&str>,
    page_title: Option<&str>,
) -> bool {
    let normalized_instruction = instruction.to_ascii_lowercase();
    let normalized_url = page_url.unwrap_or_default().to_ascii_lowercase();
    let normalized_title = page_title.unwrap_or_default().to_ascii_lowercase();

    let is_zhihu = normalized_instruction.contains("zhihu")
        || instruction.contains("知乎")
        || normalized_url.contains("zhihu.com")
        || normalized_title.contains("zhihu")
        || page_title.unwrap_or_default().contains("知乎");
    let is_write = normalized_instruction.contains("article")
        || normalized_instruction.contains("write")
        || normalized_instruction.contains("publish")
        || instruction.contains("文章")
        || instruction.contains("写")
        || instruction.contains("发布")
        || instruction.contains("发表")
        || normalized_url.contains("creator")
        || normalized_url.contains("write")
        || page_title.unwrap_or_default().contains("创作")
        || page_title.unwrap_or_default().contains("写文章");

    is_zhihu && is_write
}

fn load_runtime_skills(config: &ZeroClawConfig, skills_dirs: &[PathBuf]) -> Vec<zeroclaw::skills::Skill> {
    let default_skills_dir = config.workspace_dir.join("skills");

    // When using only the default workspace skills directory, use the
    // config-aware loader which respects open_skills configuration.
    if skills_dirs.len() == 1 && skills_dirs[0] == default_skills_dir {
        return zeroclaw::skills::load_skills_with_config(&config.workspace_dir, config);
    }

    // Start with workspace skills, then filter out those from the default dir
    // so they don't duplicate skills loaded from the configured directories.
    let mut skills = zeroclaw::skills::load_skills_with_config(&config.workspace_dir, config);
    skills.retain(|skill| {
        skill
            .location
            .as_ref()
            .map(|location| !location.starts_with(&default_skills_dir))
            .unwrap_or(true)
    });

    for dir in skills_dirs {
        if *dir == default_skills_dir {
            continue;
        }
        skills.extend(zeroclaw::skills::load_skills_from_directory(
            dir,
            config.skills.allow_scripts,
        ));
    }
    skills
}

fn build_page_context_message(page_url: Option<&str>, page_title: Option<&str>) -> Option<String> {
    let mut parts = Vec::new();
    if let Some(page_url) = page_url.map(str::trim).filter(|value| !value.is_empty()) {
        parts.push(format!("Current page URL: {page_url}"));
    }
    if let Some(page_title) = page_title.map(str::trim).filter(|value| !value.is_empty()) {
        parts.push(format!("Current page title: {page_title}"));
    }

    if parts.is_empty() {
        return None;
    }

    Some(format!("Current browser context:\n{}", parts.join("\n")))
}

fn map_anyhow_to_pipe_error(err: anyhow::Error) -> PipeError {
    PipeError::Protocol(err.to_string())
}
