use std::fmt;
use std::fs;
use std::path::{Path, PathBuf};

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SceneKind {
    ReportCollection,
    Monitoring,
}

impl SceneKind {
    pub fn from_str(s: &str) -> Option<Self> {
        match s {
            "report_collection" => Some(Self::ReportCollection),
            "monitoring" => Some(Self::Monitoring),
            _ => None,
        }
    }

    pub fn as_str(&self) -> &'static str {
        match self {
            Self::ReportCollection => "report_collection",
            Self::Monitoring => "monitoring",
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ToolKind {
    BrowserScript,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BootstrapAnalysis {
    pub target_url: Option<String>,
    pub expected_domain: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SceneSourceAnalysis {
    pub scene_kind: SceneKind,
    pub tool_kind: ToolKind,
    pub bootstrap: BootstrapAnalysis,
    pub collection_entry_script: Option<String>,
    pub source_dir: PathBuf,
}

#[derive(Debug)]
pub struct AnalyzeSceneError {
    message: String,
}

impl AnalyzeSceneError {
    fn new(message: impl Into<String>) -> Self {
        Self {
            message: message.into(),
        }
    }
}

impl fmt::Display for AnalyzeSceneError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.message)
    }
}

impl std::error::Error for AnalyzeSceneError {}

/// Analyze scene source with an optional scene kind hint.
///
/// The hint parameter takes priority over meta tags.
/// If neither hint nor meta tag is present, defaults to ReportCollection.
pub fn analyze_scene_source_with_hint(
    source_dir: &Path,
    scene_kind_hint: Option<SceneKind>,
) -> Result<SceneSourceAnalysis, AnalyzeSceneError> {
    let index_path = source_dir.join("index.html");
    let html = fs::read_to_string(&index_path).map_err(|err| {
        AnalyzeSceneError::new(format!(
            "failed to read scene source {}: {err}",
            index_path.display()
        ))
    })?;

    // Determine scene kind: hint > meta > default
    let scene_kind = if let Some(hint) = scene_kind_hint {
        hint
    } else {
        let meta_kind = meta_content(&html, "sgclaw-scene-kind");
        meta_kind
            .as_deref()
            .and_then(SceneKind::from_str)
            .unwrap_or(SceneKind::ReportCollection)
    };

    // Tool kind is currently only browser_script
    let tool_kind = meta_content(&html, "sgclaw-tool-kind");
    if let Some(ref tk) = tool_kind {
        if tk != "browser_script" {
            return Err(AnalyzeSceneError::new(format!(
                "unsupported tool kind: {}",
                tk
            )));
        }
    }
    // Default tool kind to BrowserScript

    let target_url = meta_content(&html, "sgclaw-target-url");
    let expected_domain = meta_content(&html, "sgclaw-expected-domain");
    let entry_script = meta_content(&html, "sgclaw-entry-script");

    // Auto-extract expected_domain from external script URLs if not provided via meta tag
    let expected_domain = expected_domain.or_else(|| extract_domain_from_external_scripts(&html));

    // All fields are optional - generator will use placeholders for missing values
    // This allows third-party scenes without meta tags to be processed

    Ok(SceneSourceAnalysis {
        scene_kind,
        tool_kind: ToolKind::BrowserScript,
        bootstrap: BootstrapAnalysis {
            target_url,
            expected_domain,
        },
        collection_entry_script: entry_script,
        source_dir: source_dir.to_path_buf(),
    })
}

/// Analyze scene source (compatibility wrapper).
///
/// Requires meta tags to be present. For new code, use `analyze_scene_source_with_hint`.
pub fn analyze_scene_source(source_dir: &Path) -> Result<SceneSourceAnalysis, AnalyzeSceneError> {
    analyze_scene_source_with_hint(source_dir, None)
}

fn meta_content(html: &str, name: &str) -> Option<String> {
    for tag in html
        .split('<')
        .filter(|fragment| fragment.starts_with("meta"))
    {
        let tag = tag.split('>').next().unwrap_or(tag);
        if attr_value(tag, "name").as_deref() == Some(name) {
            return attr_value(tag, "content").map(|value| value.trim().to_string());
        }
    }
    None
}

fn attr_value(tag: &str, attr: &str) -> Option<String> {
    let needle = format!("{attr}=");
    let start = tag.find(&needle)? + needle.len();
    let rest = &tag[start..];
    let quote = rest.chars().next()?;
    if quote != '"' && quote != '\'' {
        return None;
    }
    let rest = &rest[quote.len_utf8()..];
    let end = rest.find(quote)?;
    Some(rest[..end].to_string())
}

/// Extract domain from the first external script URL found in HTML.
///
/// Looks for `<script src="http://...">` or `<script src="https://...">` tags
/// and extracts the host:port portion as expected_domain.
fn extract_domain_from_external_scripts(html: &str) -> Option<String> {
    for tag in html
        .split('<')
        .filter(|fragment| fragment.starts_with("script"))
    {
        if let Some(src) = attr_value(tag, "src") {
            // Check if it's an external URL (http:// or https://)
            if src.starts_with("http://") || src.starts_with("https://") {
                // Extract domain (host:port) from URL
                if let Some(domain) = extract_domain_from_url(&src) {
                    return Some(domain);
                }
            }
        }
    }
    None
}

/// Extract domain (host:port) from a URL string.
fn extract_domain_from_url(url: &str) -> Option<String> {
    // Remove protocol prefix
    let rest = url
        .strip_prefix("http://")
        .or_else(|| url.strip_prefix("https://"))?;
    
    // Find the end of domain (first '/' or end of string)
    let domain_end = rest.find('/').unwrap_or(rest.len());
    let domain = &rest[..domain_end];
    
    // Return non-empty domain
    if domain.is_empty() {
        None
    } else {
        Some(domain.to_string())
    }
}
