use console::style;
use hyper::StatusCode;
use serde::Deserialize;
use util::full_file_path;

use std::{collections::HashMap, path::Path};

mod util;

use crate::core::{
    server::{
        parsed_request::ParsedRequest,
        response::{
            error_response::internal_server_error_response,
            file_response::FileResponse,
            status_code_response::{status_code_response, status_code_response_with_message},
            text_response::text_response,
        },
        types::BoxBody,
    },
    util::http::delay_response,
};

#[derive(Clone, Deserialize, Debug)]
pub struct Respond {
    pub file_path: Option<String>,
    pub csv_records_key: Option<String>,
    pub text: Option<String>,
    pub status: Option<u16>,
    #[serde(skip)]
    pub status_code: Option<StatusCode>,
    pub headers: Option<HashMap<String, Option<String>>>,
    pub delay_response_milliseconds: Option<u32>,
}

impl Respond {
    /// Produce the HTTP response for a matched rule.
    ///
    /// # Why the branches are ordered file → text → status → error
    ///
    /// The fields are mutually specialised:
    /// - `file_path` serves a file (possibly with CSV→JSON conversion).
    /// - `text` + `status` yields a custom-status text response.
    /// - `text` alone yields a plain 200 text response.
    /// - `status` alone yields an empty body with that status.
    ///
    /// The validator (`Respond::validate`) already rejects combinations
    /// that don't make sense (e.g. `file_path` + `text`). At runtime we
    /// therefore only need to check the most specific field first and
    /// fall through — this also keeps the `Err` branch minimal, since
    /// reaching it means something slipped past validation.
    pub async fn response(
        &self,
        dir_prefix: &str,
        parsed_request: &ParsedRequest,
    ) -> Result<hyper::Response<BoxBody>, hyper::http::Error> {
        if let Some(delay_response_milliseconds) = self.delay_response_milliseconds {
            delay_response(delay_response_milliseconds).await;
        }

        let request_headers = &parsed_request.component_parts.headers;

        // file_path → file/CSV/JSON response
        if let Some(file_path) = self.file_path.as_ref() {
            let Some(full_file_path) = full_file_path(file_path.as_str(), dir_prefix) else {
                log::error!(
                    "{}:\n{} (prefix = {})",
                    style("file not found").red(),
                    file_path,
                    dir_prefix,
                );
                return internal_server_error_response(
                    "failed to get response file",
                    request_headers,
                );
            };

            return FileResponse::new_with_csv_records_jsonpath(
                full_file_path.as_str(),
                self.headers.as_ref(),
                self.csv_records_key.clone(),
                request_headers,
            )
            .file_content_response()
            .await;
        }

        // text (+ optional status) → text response
        if let Some(text) = self.text.as_ref() {
            return match self.status_code.as_ref() {
                Some(status_code) => status_code_response_with_message(
                    status_code,
                    text.as_str(),
                    request_headers,
                ),
                None => text_response(text.as_str(), None, self.headers.as_ref(), request_headers),
            };
        }

        // status alone → empty-body status response
        if let Some(status_code) = self.status_code.as_ref() {
            return status_code_response(status_code, request_headers);
        }

        // validator should have caught this; defensive only
        internal_server_error_response("invalid respond def", request_headers)
    }

    /// validate
    pub fn validate(&self, dir_prefix: &str, rule_idx: usize, rule_set_idx: usize) -> bool {
        let all_missing_of_file_path_text_status =
            self.file_path.is_none() && self.text.is_none() && self.status.is_none();
        if all_missing_of_file_path_text_status {
            log::error!(
                "{} at least either of: file_path, text or status (rule #{} in rule set #{})",
                style("required").red(),
                rule_idx + 1,
                rule_set_idx + 1
            );
            return false;
        }

        let duplicate_file_path_text = self.file_path.is_some() && self.text.is_some();
        if duplicate_file_path_text {
            log::error!(
                "{} set at both file_path and text (rule #{} in rule set #{})",
                style("cannot").red(),
                rule_idx + 1,
                rule_set_idx + 1
            );
            return false;
        }

        let file_path_with_status = self.file_path.is_some() && self.status.is_some();
        if file_path_with_status {
            log::error!(
                "{} use status with file_path. only with text (rule #{} in rule set #{})",
                style("cannot").red(),
                rule_idx + 1,
                rule_set_idx + 1
            );
            return false;
        }

        if let Some(file_path) = self.file_path.as_ref() {
            file_path_validate(file_path.as_str(), dir_prefix, rule_idx, rule_set_idx)
        } else {
            true
        }
    }
}

impl std::fmt::Display for Respond {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        if let Some(status_code) = self.status_code {
            let _ = writeln!(f, "status_code = {} ", status_code);
        }
        if let Some(text) = self.text.as_ref() {
            let _ = writeln!(f, "text = `{}` ", text);
        }
        if let Some(file_path) = self.file_path.as_ref() {
            let _ = writeln!(f, "file_path = `{}` ", file_path);
        }

        Ok(())
    }
}

/// validate on content with response type
fn file_path_validate(
    file_path: &str,
    dir_prefix: &str,
    rule_idx: usize,
    rule_set_idx: usize,
) -> bool {
    let p = Path::new(dir_prefix).join(file_path);
    let ret = p.exists();
    if !ret {
        log::error!(
            "{} (rule #{} in rule set #{}):\n`{}`",
            style("file not found").red(),
            rule_idx + 1,
            rule_set_idx + 1,
            p.to_str().unwrap_or_default(),
        );
    }
    ret
}
