use std::{
    collections::{BTreeSet, HashMap},
    fs,
    path::{Path, PathBuf},
    sync::{Arc, OnceLock},
};

use tokio::sync::{OwnedSemaphorePermit, Semaphore};

use crate::{
    collection::Collection,
    error::{HenError, HenErrorKind, HenResult},
    parser::{
        self,
        context::{self, PromptMode},
        SyntaxSummary,
    },
    prompt_generator::scan_directory,
    request::{
        self, ExecutionOptions, ExecutionRecord, FormDataType, Request, RequestFailure,
        RequestPlanner,
    },
};

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct PromptRequirement {
    pub name: String,
    pub default: Option<String>,
}

#[derive(Debug, Clone)]
pub struct RequestSummary {
    pub index: usize,
    pub description: String,
    pub method: String,
    pub url: String,
    pub protocol: String,
    pub protocol_context: Option<serde_json::Value>,
    pub dependencies: Vec<String>,
}

#[derive(Debug, Clone)]
pub struct CollectionSummary {
    pub path: PathBuf,
    pub name: String,
    pub description: String,
    pub requests: Vec<RequestSummary>,
    pub required_inputs: Vec<PromptRequirement>,
}

#[derive(Debug, Clone)]
pub struct RunRequest {
    pub path: PathBuf,
    pub selector: Option<String>,
    pub inputs: HashMap<String, String>,
    pub execution_options: ExecutionOptions,
}

pub struct PreparedRun {
    _prompt_session: PromptSession,
    pub collection: CollectionSummary,
    pub requests: Vec<Request>,
    pub plan: Vec<usize>,
    pub selected_requests: Vec<usize>,
    pub primary_target: Option<usize>,
}

#[derive(Debug, Clone)]
pub struct RunOutcome {
    pub collection: CollectionSummary,
    pub plan: Vec<usize>,
    pub selected_requests: Vec<usize>,
    pub primary_target: Option<usize>,
    pub records: Vec<ExecutionRecord>,
    pub failures: Vec<RequestFailure>,
    pub execution_failed: bool,
    pub interrupted: Option<request::InterruptSignal>,
}

#[derive(Debug, Clone)]
pub struct VerificationResult {
    pub path: Option<PathBuf>,
    pub summary: SyntaxSummary,
    pub required_inputs: Vec<PromptRequirement>,
}

struct CollectionInput {
    path: PathBuf,
    source: String,
    working_dir: PathBuf,
}

struct PromptSession {
    _permit: OwnedSemaphorePermit,
    previous_mode: PromptMode,
}

impl PromptSession {
    async fn acquire(inputs: HashMap<String, String>) -> HenResult<Self> {
        let permit = prompt_gate().clone().acquire_owned().await.map_err(|err| {
            HenError::new(HenErrorKind::Execution, "Prompt input gate is unavailable")
                .with_detail(err.to_string())
        })?;

        let previous_mode = context::prompt_mode();
        context::set_prompt_mode(PromptMode::NonInteractive);
        context::set_prompt_inputs(inputs);

        Ok(Self {
            _permit: permit,
            previous_mode,
        })
    }
}

impl Drop for PromptSession {
    fn drop(&mut self) {
        context::set_prompt_inputs(HashMap::new());
        context::set_prompt_mode(self.previous_mode);
    }
}

pub async fn run_path(config: RunRequest) -> HenResult<RunOutcome> {
    let execution_options = config.execution_options.clone();
    let prepared = prepare_run_path(config).await?;

    let execution_result =
        request::execute_request_plan(&prepared.requests, &prepared.plan, execution_options).await;

    let (records, failures, execution_failed) = match execution_result {
        Ok(records) => (records, Vec::new(), false),
        Err(err) => {
            let (failures, completed) = err.into_parts();
            (completed, failures, true)
        }
    };

    Ok(finish_run(
        prepared,
        records,
        failures,
        execution_failed,
        None,
    ))
}

pub async fn prepare_run_path(config: RunRequest) -> HenResult<PreparedRun> {
    let prompt_session = PromptSession::acquire(config.inputs).await?;

    let input = load_collection_input(&config.path)?;
    let collection = parse_collection_input(&input)?;

    let planner = RequestPlanner::new(&collection.requests).map_err(|err| {
        HenError::new(
            HenErrorKind::Planner,
            "Failed to build request dependency graph",
        )
        .with_detail(err.to_string())
    })?;

    let (plan, selected_requests, primary_target) =
        resolve_execution_plan_non_interactive(&collection, &planner, config.selector.as_deref())?;

    validate_plan_prompt_inputs(&collection.requests, &plan)?;

    let collection_summary = summarize_collection(
        &input.path,
        &input.source,
        &input.working_dir,
        &collection,
    )?;

    Ok(PreparedRun {
        _prompt_session: prompt_session,
        collection: collection_summary,
        requests: collection.requests,
        plan,
        selected_requests,
        primary_target,
    })
}

pub fn finish_run(
    prepared: PreparedRun,
    records: Vec<ExecutionRecord>,
    failures: Vec<RequestFailure>,
    execution_failed: bool,
    interrupted: Option<request::InterruptSignal>,
) -> RunOutcome {
    RunOutcome {
        collection: prepared.collection,
        plan: prepared.plan,
        selected_requests: prepared.selected_requests,
        primary_target: prepared.primary_target,
        records,
        failures,
        execution_failed,
        interrupted,
    }
}

pub fn verify_path(path: PathBuf) -> HenResult<VerificationResult> {
    let input = load_collection_input(&path)?;
    verify_input(Some(input.path.clone()), &input.source, input.working_dir)
}

pub fn verify_source(source: String, working_dir: PathBuf) -> HenResult<VerificationResult> {
    verify_input(None, &source, working_dir)
}

fn verify_input(
    path: Option<PathBuf>,
    source: &str,
    working_dir: PathBuf,
) -> HenResult<VerificationResult> {
    let summary =
        parser::inspect_collection_syntax(source, working_dir.clone()).map_err(HenError::from)?;
    let required_inputs = collect_required_inputs(source, &working_dir)?;

    Ok(VerificationResult {
        path,
        summary,
        required_inputs,
    })
}

fn prompt_gate() -> &'static Arc<Semaphore> {
    static PROMPT_GATE: OnceLock<Arc<Semaphore>> = OnceLock::new();
    PROMPT_GATE.get_or_init(|| Arc::new(Semaphore::new(1)))
}

fn load_collection_input(path: &Path) -> HenResult<CollectionInput> {
    let resolved_path = resolve_collection_path(path)?;
    let source = fs::read_to_string(&resolved_path).map_err(|err| {
        HenError::new(
            HenErrorKind::Io,
            format!("Failed to read collection {}", resolved_path.display()),
        )
        .with_detail(err.to_string())
    })?;

    let working_dir =
        resolved_path
            .parent()
            .map(Path::to_path_buf)
            .unwrap_or(std::env::current_dir().map_err(|err| {
                HenError::new(HenErrorKind::Io, "Failed to determine current directory")
                    .with_detail(err.to_string())
            })?);

    Ok(CollectionInput {
        path: resolved_path,
        source,
        working_dir,
    })
}

fn resolve_collection_path(path: &Path) -> HenResult<PathBuf> {
    if path.is_dir() {
        let hen_files = scan_directory(path.to_path_buf()).map_err(|err| {
            err.with_detail(format!("While scanning directory {}", path.display()))
        })?;

        return match hen_files.len() {
            0 => Err(
                HenError::new(HenErrorKind::Input, "No .hen files found in directory")
                    .with_detail(format!("Directory: {}", path.display())),
            ),
            1 => Ok(hen_files[0].clone()),
            _ => Err(HenError::new(
                HenErrorKind::Input,
                "Directory contains multiple .hen files and cannot be resolved non-interactively",
            )
            .with_detail(format!("Directory: {}", path.display()))
            .with_detail("Provide a specific collection file path instead.")),
        };
    }

    Ok(path.to_path_buf())
}

fn parse_collection_input(input: &CollectionInput) -> HenResult<Collection> {
    parser::parse_collection(&input.source, input.working_dir.clone())
        .map_err(HenError::from)
        .map_err(|err| {
            err.with_detail(format!(
                "While parsing collection file {}",
                input.path.display()
            ))
        })
}

fn summarize_collection(
    path: &Path,
    source: &str,
    working_dir: &PathBuf,
    collection: &Collection,
) -> HenResult<CollectionSummary> {
    let required_inputs = collect_required_inputs(source, working_dir)?;
    let requests = collection
        .requests
        .iter()
        .enumerate()
        .map(|(index, request)| {
            let http = request.http_operation();
            RequestSummary {
                index,
                description: request.description.clone(),
                method: http.method.as_str().to_string(),
                url: http.url.clone(),
                protocol: request.protocol().as_str().to_string(),
                protocol_context: request.protocol_context_json(),
                dependencies: request.dependencies.clone(),
            }
        })
        .collect();

    Ok(CollectionSummary {
        path: path.to_path_buf(),
        name: collection.name.clone(),
        description: collection.description.clone(),
        requests,
        required_inputs,
    })
}

fn collect_required_inputs(
    source: &str,
    working_dir: &PathBuf,
) -> HenResult<Vec<PromptRequirement>> {
    let preprocessed = parser::preprocess_only(source, working_dir).map_err(|err| {
        HenError::new(HenErrorKind::Parse, "Failed to preprocess hen file").with_detail(err)
    })?;

    let mut prompts = BTreeSet::new();
    for (name, default) in context::extract_prompt_placeholders(&preprocessed) {
        prompts.insert(PromptRequirement { name, default });
    }

    Ok(prompts.into_iter().collect())
}

pub fn validate_plan_prompt_inputs(requests: &[Request], plan: &[usize]) -> HenResult<()> {
    let mut missing = BTreeSet::new();

    for &idx in plan {
        let request = requests.get(idx).ok_or_else(|| {
            HenError::new(
                HenErrorKind::Planner,
                "Execution plan referenced an unknown request",
            )
            .with_detail(format!("Index: {}", idx))
        })?;

        collect_request_missing_prompts(request, &mut missing);
    }

    if missing.is_empty() {
        return Ok(());
    }

    let details = missing
        .into_iter()
        .map(|requirement| match requirement.default {
            Some(default) => format!(
                "Missing value for prompt '{}' (default: {})",
                requirement.name, default
            ),
            None => format!("Missing value for prompt '{}'", requirement.name),
        })
        .collect::<Vec<_>>()
        .join("\n");

    Err(HenError::new(
        HenErrorKind::Input,
        "One or more required prompt inputs were not supplied",
    )
    .with_detail(details)
    .with_exit_code(2))
}

fn collect_request_missing_prompts(request: &Request, missing: &mut BTreeSet<PromptRequirement>) {
    let http = request.http_operation();

    collect_missing_prompts_from_str(&http.url, missing);

    for value in http.headers.values() {
        collect_missing_prompts_from_str(value, missing);
    }

    for value in http.query_params.values() {
        collect_missing_prompts_from_str(value, missing);
    }

    for value in http.form_data.values() {
        match value {
            FormDataType::Text(text) => collect_missing_prompts_from_str(text, missing),
            FormDataType::File(path) => {
                let path = path.to_string_lossy();
                collect_missing_prompts_from_str(path.as_ref(), missing);
            }
        }
    }

    if let Some(body) = &http.body {
        collect_missing_prompts_from_str(body, missing);
    }

    if let Some(content_type) = &http.body_content_type {
        collect_missing_prompts_from_str(content_type, missing);
    }

    if let Some(graphql) = request.graphql_operation() {
        if let Some(operation_name) = &graphql.operation_name {
            collect_missing_prompts_from_str(operation_name, missing);
        }
        collect_missing_prompts_from_str(&graphql.document, missing);
        if let Some(variables_json) = &graphql.variables_json {
            collect_missing_prompts_from_str(variables_json, missing);
        }
    }

    if let Some(mcp) = request.mcp_operation() {
        match &mcp.call {
            crate::request::McpCall::Initialize(initialize) => {
                if let Some(protocol_version) = &initialize.protocol_version {
                    collect_missing_prompts_from_str(protocol_version, missing);
                }
                if let Some(client_name) = &initialize.client_name {
                    collect_missing_prompts_from_str(client_name, missing);
                }
                if let Some(client_version) = &initialize.client_version {
                    collect_missing_prompts_from_str(client_version, missing);
                }
                if let Some(capabilities_json) = &initialize.capabilities_json {
                    collect_missing_prompts_from_str(capabilities_json, missing);
                }
            }
            crate::request::McpCall::ToolsList | crate::request::McpCall::ResourcesList => {}
            crate::request::McpCall::ToolsCall(tool_call) => {
                collect_missing_prompts_from_str(&tool_call.tool_name, missing);
                if let Some(arguments_json) = &tool_call.arguments_json {
                    collect_missing_prompts_from_str(arguments_json, missing);
                }
            }
        }
    }
}

fn collect_missing_prompts_from_str(value: &str, missing: &mut BTreeSet<PromptRequirement>) {
    for (name, default) in context::extract_prompt_placeholders(value) {
        if !context::has_prompt_input(&name) {
            missing.insert(PromptRequirement { name, default });
        }
    }
}

fn resolve_execution_plan_non_interactive(
    collection: &Collection,
    planner: &RequestPlanner,
    selector: Option<&str>,
) -> HenResult<(Vec<usize>, Vec<usize>, Option<usize>)> {
    match selector {
        Some("all") => {
            let order = planner.order_all();
            Ok((order.clone(), order, None))
        }
        Some(selector) => {
            let idx = selector.parse::<usize>().map_err(|_| {
                HenError::new(HenErrorKind::Input, "Selector must be an integer or 'all'")
                    .with_detail(format!("Received: {}", selector))
                    .with_exit_code(2)
            })?;

            let order = planner.order_for(idx).map_err(|err| {
                HenError::new(HenErrorKind::Planner, "Failed to plan for selected request")
                    .with_detail(err.to_string())
            })?;

            Ok((order.clone(), vec![idx], Some(idx)))
        }
        None => {
            if collection.requests.len() == 1 {
                let order = planner.order_for(0).map_err(|err| {
                    HenError::new(HenErrorKind::Planner, "Failed to plan for the only request")
                        .with_detail(err.to_string())
                })?;

                Ok((order.clone(), vec![0], Some(0)))
            } else {
                Err(HenError::new(
                    HenErrorKind::Input,
                    "A selector is required when a collection contains multiple requests",
                )
                .with_detail("Provide a request index or use 'all'.")
                .with_exit_code(2))
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::request::{
        GraphqlOperation, HttpOperation, McpCall, McpOperation, McpToolCall, RequestOperation,
    };
    use http::Method;
    use std::{collections::HashMap, path::PathBuf};

    #[test]
    fn verify_source_rejects_invalid_assertion_operator() {
        let working_dir = std::env::current_dir().unwrap();
        let source = r#"
---

Broken assertion

GET https://example.com

^ & body =~ /foo/
"#;

        let error = verify_source(source.to_string(), working_dir).unwrap_err();

        assert_eq!(error.kind(), HenErrorKind::Parse);
        assert!(error
            .to_string()
            .contains("No valid operator found in assertion"));
    }

    #[test]
    fn verify_source_accepts_valid_matches_assertion() {
        let working_dir = std::env::current_dir().unwrap();
        let source = r#"
---

Valid assertion

GET https://example.com

^ & body ~= /foo/
"#;

        let result = verify_source(source.to_string(), working_dir).unwrap();

        assert_eq!(result.summary.requests.len(), 1);
        assert_eq!(result.summary.requests[0].method, "GET");
        assert_eq!(result.summary.requests[0].url, "https://example.com");
    }

    #[test]
    fn validate_plan_prompt_inputs_includes_graphql_document_and_variables() {
        let request = Request {
            description: "GraphQL request".into(),
            base_description: "GraphQL request".into(),
            operation: RequestOperation::Graphql(GraphqlOperation {
                http: HttpOperation {
                    method: Method::POST,
                    url: "https://example.com/graphql".into(),
                    headers: HashMap::new(),
                    query_params: HashMap::new(),
                    form_data: HashMap::new(),
                    body: None,
                    body_content_type: None,
                },
                operation_name: Some("[[ OP_NAME ]]".into()),
                document: "query [[ QUERY_NAME ]] { viewer { id } }".into(),
                variables_json: Some(r#"{"id": "[[ USER_ID ]]"}"#.into()),
            }),
            session_name: None,
            callback_src: vec![],
            response_captures: vec![],
            assertions: vec![],
            declared_dependencies: vec![],
            dependencies: vec![],
            context: HashMap::new(),
            working_dir: PathBuf::new(),
            map_iteration: None,
        };

        let err = validate_plan_prompt_inputs(&[request], &[0]).expect_err("should fail");
        let message = err.to_string();

        assert!(message.contains("OP_NAME"));
        assert!(message.contains("QUERY_NAME"));
        assert!(message.contains("USER_ID"));
    }

    #[test]
    fn validate_plan_prompt_inputs_includes_mcp_tool_call_fields() {
        let request = Request {
            description: "MCP request".into(),
            base_description: "MCP request".into(),
            operation: RequestOperation::Mcp(McpOperation {
                http: HttpOperation {
                    method: Method::POST,
                    url: "https://example.com/mcp".into(),
                    headers: HashMap::new(),
                    query_params: HashMap::new(),
                    form_data: HashMap::new(),
                    body: None,
                    body_content_type: None,
                },
                call: McpCall::ToolsCall(McpToolCall {
                    tool_name: "[[ TOOL_NAME ]]".into(),
                    arguments_json: Some(r#"{"query": "[[ QUERY ]]"}"#.into()),
                }),
            }),
            session_name: Some("app".into()),
            callback_src: vec![],
            response_captures: vec![],
            assertions: vec![],
            declared_dependencies: vec![],
            dependencies: vec![],
            context: HashMap::new(),
            working_dir: PathBuf::new(),
            map_iteration: None,
        };

        let err = validate_plan_prompt_inputs(&[request], &[0]).expect_err("should fail");
        let message = err.to_string();

        assert!(message.contains("TOOL_NAME"));
        assert!(message.contains("QUERY"));
    }
}
