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

use crate::generated_scene::analyzer::{
    analyze_scene_source_with_hint, AnalyzeSceneError, SceneKind, SceneSourceAnalysis,
};
use crate::generated_scene::lessons::{
    load_generation_lessons, GenerationLessons, BUILTIN_REPORT_COLLECTION_LESSONS,
};

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ApiEndpointJson {
    pub name: String,
    pub url: String,
    #[serde(default)]
    pub method: String,
    #[serde(default)]
    pub description: Option<String>,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ModeConditionJson {
    pub field: String,
    #[serde(default = "default_equals")]
    pub operator: String,
    pub value: serde_json::Value,
}

fn default_equals() -> String {
    "equals".to_string()
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct NormalizeRulesJson {
    #[serde(rename = "type", default = "default_validate_all")]
    pub rules_type: String,
    #[serde(rename = "requiredFields", default)]
    pub required_fields: Vec<String>,
    #[serde(rename = "filterNull", default = "default_true")]
    pub filter_null: bool,
}

fn default_validate_all() -> String {
    "validate_all_columns".to_string()
}

fn default_true() -> bool {
    true
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ApiEndpointEnhancedJson {
    pub name: String,
    pub url: String,
    #[serde(default)]
    pub method: String,
    #[serde(rename = "contentType", default)]
    pub content_type: Option<String>,
    #[serde(default)]
    pub description: Option<String>,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ModeConfigJson {
    pub name: String,
    #[serde(default)]
    pub label: Option<String>,
    pub condition: ModeConditionJson,
    #[serde(rename = "apiEndpoint")]
    pub api_endpoint: ApiEndpointEnhancedJson,
    #[serde(rename = "columnDefs", default)]
    pub column_defs: Vec<(String, String)>,
    #[serde(rename = "requestTemplate", default)]
    pub request_template: Option<serde_json::Value>,
    #[serde(rename = "normalizeRules", default)]
    pub normalize_rules: Option<NormalizeRulesJson>,
    #[serde(rename = "responsePath", default)]
    pub response_path: Option<String>,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct BusinessLogicJson {
    #[serde(default)]
    pub data_fetch: Option<String>,
    #[serde(default)]
    pub data_transform: Option<String>,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SceneInfoJson {
    #[serde(rename = "sceneId")]
    pub scene_id: String,
    #[serde(rename = "sceneName")]
    pub scene_name: String,
    #[serde(rename = "sceneKind", default)]
    pub scene_kind: String,
    #[serde(rename = "sourceSystem", default)]
    pub source_system: Option<String>,
    #[serde(rename = "expectedDomain", default)]
    pub expected_domain: Option<String>,
    #[serde(rename = "targetUrl", default)]
    pub target_url: Option<String>,
    #[serde(rename = "apiEndpoints", default)]
    pub api_endpoints: Vec<ApiEndpointJson>,
    #[serde(rename = "staticParams", default)]
    pub static_params: HashMap<String, String>,
    #[serde(rename = "columnDefs", default)]
    pub column_defs: Vec<(String, String)>,
    #[serde(rename = "entryMethod", default)]
    pub entry_method: Option<String>,
    #[serde(rename = "businessLogic", default)]
    pub business_logic: Option<BusinessLogicJson>,
    // Multi-mode support (new fields)
    #[serde(default)]
    pub modes: Vec<ModeConfigJson>,
    #[serde(rename = "defaultMode", default)]
    pub default_mode: Option<String>,
    #[serde(rename = "modeSwitchField", default)]
    pub mode_switch_field: Option<String>,
}

#[derive(Debug, Clone)]
pub struct GenerateSceneRequest {
    pub source_dir: PathBuf,
    pub scene_id: String,
    pub scene_name: String,
    pub scene_kind: Option<SceneKind>,
    pub target_url: Option<String>,
    pub output_root: PathBuf,
    pub lessons_path: Option<PathBuf>,
    pub scene_info_json: Option<SceneInfoJson>,
}

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

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

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

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

impl From<AnalyzeSceneError> for GenerateSceneError {
    fn from(value: AnalyzeSceneError) -> Self {
        Self::new(value.to_string())
    }
}

pub fn generate_scene_package(
    request: GenerateSceneRequest,
) -> Result<PathBuf, GenerateSceneError> {
    let analysis = analyze_scene_source_with_hint(&request.source_dir, request.scene_kind.clone())?;
    let (lessons, lessons_source): (GenerationLessons, String) = match &request.lessons_path {
        Some(path) => (
            load_generation_lessons(path).map_err(GenerateSceneError::new)?,
            path.display().to_string(),
        ),
        None => (
            GenerationLessons::default_report_collection(),
            BUILTIN_REPORT_COLLECTION_LESSONS.to_string(),
        ),
    };
    if !lessons.routing.require_exact_suffix
        || !lessons.bootstrap.require_expected_domain
        || !lessons.bootstrap.require_target_url
        || !lessons.artifact.require_report_artifact
    {
        return Err(GenerateSceneError::new(
            "generation lessons do not allow a registration-ready report scene",
        ));
    }

    let tool_name = format!("collect_{}", sanitize_ident(&request.scene_id));
    let skill_root = request.output_root.join("skills").join(&request.scene_id);
    let scripts_dir = skill_root.join("scripts");
    let references_dir = skill_root.join("references");
    fs::create_dir_all(&scripts_dir).map_err(|err| io_error("create", &scripts_dir, err))?;
    fs::create_dir_all(&references_dir).map_err(|err| io_error("create", &references_dir, err))?;

    write_file(
        &skill_root.join("SKILL.toml"),
        &skill_toml(&request, &analysis, &tool_name),
    )?;
    write_file(
        &skill_root.join("SKILL.md"),
        &skill_md(&request, &analysis, &tool_name),
    )?;
    write_file(
        &skill_root.join("scene.toml"),
        &scene_toml(&request, &analysis, &tool_name),
    )?;
    write_file(
        &references_dir.join("org-dictionary.json"),
        default_org_dictionary(),
    )?;
    write_file(
        &scripts_dir.join(format!("{tool_name}.js")),
        &browser_script(&request.scene_id, &analysis, request.scene_info_json.as_ref()),
    )?;
    write_file(
        &scripts_dir.join(format!("{tool_name}.test.js")),
        &browser_script_test(&tool_name, &analysis),
    )?;
    write_file(
        &references_dir.join("generation-lessons.md"),
        &format!(
            "# Generation Lessons\n\nGenerated from `{}` with lessons `{}`.\n\nThis package is limited to report/collection browser_script scenes.\n",
            request.source_dir.display(),
            lessons_source
        ),
    )?;

    if let Some(entry_script) = analysis.collection_entry_script.as_deref() {
        let source_script = request.source_dir.join(entry_script);
        if source_script.exists() {
            let copied = fs::read_to_string(&source_script)
                .map_err(|err| io_error("read", &source_script, err))?;
            write_file(&references_dir.join("source-entry-script.js"), &copied)?;
        }
    }

    Ok(skill_root)
}

fn write_file(path: &Path, content: &str) -> Result<(), GenerateSceneError> {
    fs::write(path, content).map_err(|err| io_error("write", path, err))
}

fn io_error(action: &str, path: &Path, err: std::io::Error) -> GenerateSceneError {
    GenerateSceneError::new(format!("failed to {action} {}: {err}", path.display()))
}

fn skill_toml(request: &GenerateSceneRequest, analysis: &SceneSourceAnalysis, tool_name: &str) -> String {
    let category = analysis.scene_kind.as_str();
    format!(
        "[skill]\nname = \"{}\"\ndescription = \"Generated {} skill: {}\"\nversion = \"0.1.0\"\n\n[[tools]]\nname = \"{}\"\ndescription = \"Collect {} scene artifact\"\nkind = \"browser_script\"\ncommand = \"scripts/{}.js\"\n",
        request.scene_id, category, request.scene_name, tool_name, request.scene_name, tool_name
    )
}

fn skill_md(request: &GenerateSceneRequest, analysis: &SceneSourceAnalysis, tool_name: &str) -> String {
    let category = analysis.scene_kind.as_str();
    format!(
        "# {}\n\nGenerated v1 {} browser_script scene skill.\n\n- Scene id: `{}`\n- Tool: `{}`\n- Runtime contract: manifest-driven deterministic routing, canonical args, generic `report-artifact` output.\n",
        request.scene_name, category, request.scene_id, tool_name
    )
}

fn scene_toml(
    request: &GenerateSceneRequest,
    analysis: &SceneSourceAnalysis,
    tool_name: &str,
) -> String {
    match analysis.scene_kind {
        SceneKind::ReportCollection => scene_toml_report_collection(request, analysis, tool_name),
        SceneKind::Monitoring => scene_toml_monitoring(request, analysis, tool_name),
    }
}

fn scene_toml_report_collection(
    request: &GenerateSceneRequest,
    analysis: &SceneSourceAnalysis,
    tool_name: &str,
) -> String {
    let expected_domain = analysis.bootstrap.expected_domain.as_deref().unwrap_or_default();
    // Use request.target_url if provided, otherwise fall back to analysis
    let target_url = request.target_url.as_deref()
        .or(analysis.bootstrap.target_url.as_deref())
        .unwrap_or_default();
    format!(
        "[scene]\nid = \"{}\"\nskill = \"{}\"\ntool = \"{}\"\nkind = \"browser_script\"\nversion = \"0.1.0\"\ncategory = \"report_collection\"\n\n[manifest]\nschema_version = \"1\"\n\n[bootstrap]\nexpected_domain = \"{}\"\ntarget_url = \"{}\"\npage_title_keywords = [\"报表\", \"线损\"]\nrequires_target_page = true\n\n[deterministic]\nsuffix = \"。。。\"\ninclude_keywords = [\"{}\", \"报表\", \"统计\"]\nexclude_keywords = [\"知乎\"]\n\n[[params]]\nname = \"org\"\nresolver = \"dictionary_entity\"\nrequired = true\nprompt_missing = \"已命中{}，但缺少供电单位。\"\nprompt_ambiguous = \"已命中{}，但供电单位存在歧义。\"\n\n[params.resolver_config]\ndictionary_ref = \"references/org-dictionary.json\"\noutput_label_field = \"org_label\"\noutput_code_field = \"org_code\"\n\n[[params]]\nname = \"period\"\nresolver = \"month_week_period\"\nrequired = true\nprompt_missing = \"已命中{}，但缺少统计周期。\"\nprompt_ambiguous = \"已命中{}，但统计周期存在歧义。\"\n\n[artifact]\ntype = \"report-artifact\"\nsuccess_status = [\"ok\", \"partial\", \"empty\"]\nfailure_status = [\"blocked\", \"error\"]\n\n[postprocess]\nexporter = \"xlsx_report\"\nauto_open = \"excel\"\n",
        request.scene_id,
        request.scene_id,
        tool_name,
        expected_domain,
        target_url,
        request.scene_name,
        request.scene_name,
        request.scene_name,
        request.scene_name,
        request.scene_name
    )
}

fn scene_toml_monitoring(
    request: &GenerateSceneRequest,
    analysis: &SceneSourceAnalysis,
    tool_name: &str,
) -> String {
    let expected_domain = analysis.bootstrap.expected_domain.as_deref().unwrap_or_default();
    // Use request.target_url if provided, otherwise fall back to analysis
    let target_url = request.target_url.as_deref()
        .or(analysis.bootstrap.target_url.as_deref())
        .unwrap_or_default();
    format!(
        "[scene]\nid = \"{}\"\nskill = \"{}\"\ntool = \"{}\"\nkind = \"browser_script\"\nversion = \"0.1.0\"\ncategory = \"monitoring\"\n\n[manifest]\nschema_version = \"1\"\n\n[bootstrap]\nexpected_domain = \"{}\"\ntarget_url = \"{}\"\npage_title_keywords = [\"监测\", \"状态\"]\nrequires_target_page = true\n\n[deterministic]\nsuffix = \"。。。\"\ninclude_keywords = [\"{}\", \"监测\", \"状态\"]\nexclude_keywords = [\"知乎\"]\n\n# 监测类场景参数留空，由用户手动编辑\n\n[artifact]\ntype = \"report-artifact\"\nsuccess_status = [\"ok\", \"partial\", \"empty\"]\nfailure_status = [\"blocked\", \"error\"]\n\n[postprocess]\nexporter = \"xlsx_report\"\nauto_open = \"excel\"\n",
        request.scene_id,
        request.scene_id,
        tool_name,
        expected_domain,
        target_url,
        request.scene_name
    )
}

fn default_org_dictionary() -> &'static str {
    r#"[
  {
    "label": "国网兰州供电公司",
    "code": "62401"
  }
]
"#
}

fn browser_script_skeleton(scene_id: &str, _analysis: &SceneSourceAnalysis) -> String {
    format!(
        "function normalizePayload(payload) {{\n  if (typeof payload === 'string') {{\n    try {{ return JSON.parse(payload); }} catch (_) {{ return {{}}; }}\n  }}\n  return payload && typeof payload === 'object' ? payload : {{}};\n}}\n\nasync function buildBrowserEntrypointResult(args, deps = {{}}) {{\n  const rows = typeof deps.collectRows === 'function'\n    ? await deps.collectRows(args)\n    : [{{\n        org_label: args.org_label || '',\n        org_code: args.org_code || '',\n        period_mode: args.period_mode || '',\n        period_value: args.period_value || '',\n        value: ''\n      }}];\n  return {{\n    type: 'report-artifact',\n    report_name: '{}',\n    status: rows.length > 0 ? 'ok' : 'empty',\n    period: {{\n      mode: args.period_mode,\n      mode_code: args.period_mode_code,\n      value: args.period_value,\n      payload: normalizePayload(args.period_payload)\n    }},\n    org: {{ label: args.org_label, code: args.org_code }},\n    column_defs: [\n      ['org_label', '供电单位'],\n      ['org_code', '供电单位编码'],\n      ['period_mode', '统计周期类型'],\n      ['period_value', '统计周期'],\n      ['value', '采集值']\n    ],\n    columns: ['org_label', 'org_code', 'period_mode', 'period_value', 'value'],\n    rows,\n    counts: {{ detail_rows: rows.length }},\n    partial_reasons: [],\n    reasons: []\n  }};\n}}\n\nif (typeof module !== 'undefined') {{\n  module.exports = {{ buildBrowserEntrypointResult, normalizePayload }};\n}}\n\nif (typeof args !== 'undefined') {{\n  return buildBrowserEntrypointResult(args);\n}}\n",
        scene_id
    )
}

fn browser_script(scene_id: &str, analysis: &SceneSourceAnalysis, scene_info: Option<&SceneInfoJson>) -> String {
    match scene_info {
        Some(info) if !info.modes.is_empty() => {
            browser_script_with_modes(scene_id, info)
        }
        Some(info) if !info.api_endpoints.is_empty() || !info.column_defs.is_empty() => {
            browser_script_with_business_logic(scene_id, analysis, info)
        }
        _ => browser_script_skeleton(scene_id, analysis),
    }
}

fn browser_script_with_business_logic(scene_id: &str, _analysis: &SceneSourceAnalysis, scene_info: &SceneInfoJson) -> String {
    let api_endpoints_json = serde_json::to_string_pretty(&scene_info.api_endpoints).unwrap_or_else(|_| "[]".to_string());
    let static_params_json = serde_json::to_string_pretty(&scene_info.static_params).unwrap_or_else(|_| "{}".to_string());
    let column_defs_json = serde_json::to_string_pretty(&scene_info.column_defs).unwrap_or_else(|_| "[]".to_string());
    
    let column_names: Vec<&str> = scene_info.column_defs.iter().map(|(name, _)| name.as_str()).collect();
    let columns_json = serde_json::to_string(&column_names).unwrap_or_else(|_| "[]".to_string());
    
    format!(
        r#"const API_ENDPOINTS = {api_endpoints_json};

const STATIC_PARAMS = {static_params_json};

const COLUMN_DEFS = {column_defs_json};

const COLUMNS = {columns_json};

const REPORT_NAME = '{scene_id}';

function normalizePayload(payload) {{
  if (typeof payload === 'string') {{
    try {{ return JSON.parse(payload); }} catch (_) {{ return {{}}; }}
  }}
  return payload && typeof payload === 'object' ? payload : {{}};
}}

function validateArgs(args) {{
  const errors = [];
  if (!args.org_code) errors.push('Missing org_code');
  if (!args.period_value) errors.push('Missing period_value');
  return {{ valid: errors.length === 0, errors }};
}}

function buildRequest(args, endpoint) {{
  // Use endpoint.url directly - it's already a complete URL
  const url = endpoint.url;
  const method = endpoint.method || 'POST';
  const headers = {{ 'Content-Type': 'application/json' }};
  const body = JSON.stringify({{ ...STATIC_PARAMS, ...args }});
  return {{ url, method, headers, body }};
}}

function normalizeRows(rawData) {{
  if (!Array.isArray(rawData)) return [];
  return rawData.map((row, index) => {{
    const result = {{ _index: index }};
    for (const [key, label] of COLUMN_DEFS) {{
      result[key] = row[key] ?? '';
    }}
    return result;
  }});
}}

function determineArtifactStatus({{ blockedReason = '', fatalError = '', reasons = [], rows = [] }}) {{
  if (blockedReason) return 'blocked';
  if (fatalError) return 'error';
  if (reasons.length > 0) return 'partial';
  if (!rows.length) return 'empty';
  return 'ok';
}}

function buildArtifact({{ status, blockedReason = '', fatalError = '', reasons = [], rows = [], args }}) {{
  return {{
    type: 'report-artifact',
    report_name: REPORT_NAME,
    status: status || determineArtifactStatus({{ blockedReason, fatalError, reasons, rows }}),
    period: {{
      mode: args.period_mode,
      mode_code: args.period_mode_code,
      value: args.period_value,
      payload: normalizePayload(args.period_payload)
    }},
    org: {{ label: args.org_label, code: args.org_code }},
    column_defs: COLUMN_DEFS,
    columns: COLUMNS,
    rows,
    counts: {{ detail_rows: rows.length }},
    partial_reasons: reasons.filter(r => r && !r.startsWith('api_') && !r.startsWith('validation_')),
    reasons: Array.from(new Set(reasons.filter(Boolean)))
  }};
}}

const defaultDeps = {{
  validatePageContext(args) {{
    const host = (globalThis.location?.hostname || '').trim();
    const expected = (args.expected_domain || '').trim();
    if (!host) return {{ ok: false, reason: 'page_context_unavailable' }};
    if (host !== expected) return {{ ok: false, reason: 'page_context_mismatch' }};
    return {{ ok: true }};
  }},

  async queryData(args) {{
    const endpoint = API_ENDPOINTS[0];
    if (!endpoint) throw new Error('No API endpoint configured');
    const request = buildRequest(args, endpoint);

    // Prefer jQuery (internal pages typically have it)
    if (typeof $ !== 'undefined' && typeof $.ajax === 'function') {{
      return new Promise((resolve, reject) => {{
        $.ajax({{
          url: request.url,
          type: request.method,
          data: request.body,
          contentType: 'application/json',
          dataType: 'json',
          success: resolve,
          error: (xhr, status, err) => reject(new Error(
            `API failed (${{xhr.status}}): ${{err}} | body=${{(xhr.responseText || '').substring(0, 200)}}`
          ))
        }});
      }});
    }}

    // Fallback: fetch API
    if (typeof fetch === 'function') {{
      const response = await fetch(request.url, {{
        method: request.method,
        headers: request.headers,
        body: request.method !== 'GET' ? request.body : undefined
      }});
      if (!response.ok) {{
        const text = await response.text().catch(() => '');
        throw new Error(`HTTP ${{response.status}}: ${{text.substring(0, 200)}}`);
      }}
      return response.json();
    }}

    throw new Error('No HTTP client available (need jQuery or fetch)');
  }}
}};

async function buildBrowserEntrypointResult(args, deps = defaultDeps) {{
  // 1. Parameter validation
  const validation = validateArgs(args);
  if (!validation.valid) {{
    return buildArtifact({{
      status: 'blocked',
      blockedReason: 'validation_failed',
      reasons: validation.errors,
      rows: [],
      args
    }});
  }}

  // 2. Page context validation
  const pageValidation = typeof deps.validatePageContext === 'function'
    ? deps.validatePageContext(args)
    : {{ ok: true }};
  if (!pageValidation?.ok) {{
    return buildArtifact({{
      status: 'blocked',
      blockedReason: pageValidation?.reason || 'page_context_mismatch',
      reasons: [pageValidation?.reason || 'page_context_mismatch'],
      rows: [],
      args
    }});
  }}

  // 3. Data fetching
  const reasons = [];
  let rawData = null;
  try {{
    rawData = await (deps.queryData ? deps.queryData(args) : Promise.resolve([]));
  }} catch (error) {{
    return buildArtifact({{
      status: 'error',
      fatalError: error.message,
      reasons: ['api_query_failed:' + error.message],
      rows: [],
      args
    }});
  }}

  // 4. Row normalization
  const rows = normalizeRows(rawData);
  if (rows.length === 0 && Array.isArray(rawData) && rawData.length > 0) {{
    reasons.push('row_normalization_partial');
  }}

  // 5. Build artifact
  return buildArtifact({{ reasons, rows, args }});
}}

if (typeof module !== 'undefined') {{
  module.exports = {{ buildBrowserEntrypointResult, normalizePayload, validateArgs, buildRequest, normalizeRows, buildArtifact, determineArtifactStatus, API_ENDPOINTS, STATIC_PARAMS, COLUMN_DEFS, COLUMNS, REPORT_NAME }};
}}

if (typeof args !== 'undefined') {{
  return buildBrowserEntrypointResult(args);
}}
"#,
        scene_id = scene_id,
        api_endpoints_json = api_endpoints_json,
        static_params_json = static_params_json,
        column_defs_json = column_defs_json,
        columns_json = columns_json
    )
}

fn browser_script_with_modes(scene_id: &str, scene_info: &SceneInfoJson) -> String {
    let modes_json = serde_json::to_string_pretty(&scene_info.modes).unwrap_or_else(|_| "[]".to_string());
    let default_mode = scene_info.default_mode.as_deref().unwrap_or("month");
    let mode_switch_field = scene_info.mode_switch_field.as_deref().unwrap_or("period_mode");

    format!(r#"const REPORT_NAME = '{scene_id}';
const MODES = {modes_json};
const DEFAULT_MODE = '{default_mode}';
const MODE_SWITCH_FIELD = '{mode_switch_field}';

function normalizePayload(payload) {{
  if (typeof payload === 'string') {{
    try {{ return JSON.parse(payload); }} catch (_) {{ return {{}}; }}
  }}
  return payload && typeof payload === 'object' ? payload : {{}};
}}

function validateArgs(args) {{
  const errors = [];
  if (!args.org_code) errors.push('Missing org_code');
  if (!args.period_value) errors.push('Missing period_value');
  return {{ valid: errors.length === 0, errors }};
}}

function detectMode(args) {{
  if (!MODES || MODES.length === 0) {{
    throw new Error('No modes configured for this scene');
  }}
  const modeValue = args[MODE_SWITCH_FIELD] || DEFAULT_MODE;
  return MODES.find(m => m.condition.value === modeValue) || MODES[0];
}}

function buildModeRequest(args, mode) {{
  const endpoint = mode.apiEndpoint;
  const template = mode.requestTemplate || {{}};
  const contentType = endpoint.contentType || 'application/json';
  const url = endpoint.url;
  const method = endpoint.method || 'POST';

  // Safe template resolver - supports args.fieldName and args['fieldName']
  function resolveTemplateValue(value) {{
    if (typeof value !== 'string') return value;
    if (!value.startsWith('${{') || !value.endsWith('}}')) return value;
    const expr = value.slice(2, -1).trim();
    // Support: args.fieldName
    const dotMatch = expr.match(/^args\\.(\w+)$/);
    if (dotMatch) return args[dotMatch[1]];
    // Support: args['fieldName']
    const bracketMatch = expr.match(/^args\['(\w+)'\]$/);
    if (bracketMatch) return args[bracketMatch[1]];
    // Fallback: return raw value
    return value;
  }}

  let body;
  if (contentType === 'application/x-www-form-urlencoded') {{
    body = {{}};
    for (const [key, value] of Object.entries(template)) {{
      body[key] = resolveTemplateValue(value);
    }}
    if (!body.orgno) body.orgno = args.org_code;
  }} else {{
    body = JSON.stringify({{ ...template, ...args }});
  }}

  return {{ url, method, headers: {{ 'Content-Type': contentType }}, body }};
}}

function normalizeModeRows(data, mode) {{
  const rules = mode.normalizeRules || {{ type: 'validate_all_columns', filterNull: true }};
  const columns = mode.columnDefs.map(([key]) => key);

  if (!Array.isArray(data)) return [];

  return data.map(row => {{
    const result = {{}};
    for (const key of columns) {{
      const v = row[key];
      result[key] = (v === null || v === undefined || v === '') ? '' : String(v).trim();
    }}
    return result;
  }}).filter(row => {{
    if (!rules.filterNull) return true;
    if (rules.type === 'validate_required' && rules.requiredFields) {{
      return rules.requiredFields.every(f => row[f] !== '');
    }}
    return columns.every(k => row[k] !== '');
  }});
}}

function determineArtifactStatus({{ blockedReason = '', fatalError = '', reasons = [], rows = [] }}) {{
  if (blockedReason) return 'blocked';
  if (fatalError) return 'error';
  if (reasons.length > 0) return 'partial';
  if (!rows.length) return 'empty';
  return 'ok';
}}

function buildArtifact({{ status, blockedReason = '', fatalError = '', reasons = [], rows = [], args, columnDefs, columns }}) {{
  return {{
    type: 'report-artifact',
    report_name: REPORT_NAME,
    status: status || determineArtifactStatus({{ blockedReason, fatalError, reasons, rows }}),
    period: {{
      mode: args.period_mode,
      mode_code: args.period_mode_code,
      value: args.period_value,
      payload: normalizePayload(args.period_payload)
    }},
    org: {{ label: args.org_label, code: args.org_code }},
    column_defs: columnDefs || [],
    columns: columns || [],
    rows,
    counts: {{ detail_rows: rows.length }},
    partial_reasons: reasons.filter(r => r && !r.startsWith('api_') && !r.startsWith('validation_')),
    reasons: Array.from(new Set(reasons.filter(Boolean)))
  }};
}}

const defaultDeps = {{
  validatePageContext(args) {{
    const host = (globalThis.location?.hostname || '').trim();
    const expected = (args.expected_domain || '').trim();
    if (!host) return {{ ok: false, reason: 'page_context_unavailable' }};
    if (host !== expected) return {{ ok: false, reason: 'page_context_mismatch' }};
    return {{ ok: true }};
  }},

  async queryModeData(args, mode) {{
    const endpoint = mode.apiEndpoint;
    const request = buildModeRequest(args, mode);
    const contentType = endpoint.contentType || 'application/json';

    // Prefer jQuery
    if (typeof $ !== 'undefined' && typeof $.ajax === 'function') {{
      return new Promise((resolve, reject) => {{
        $.ajax({{
          url: request.url,
          type: request.method,
          data: request.body,
          contentType: contentType,
          dataType: 'json',
          success: resolve,
          error: (xhr, status, err) => reject(new Error(
            `API failed (${{xhr.status}}): ${{err}} | body=${{(xhr.responseText || '').substring(0, 200)}}`
          ))
        }});
      }});
    }}

    // Fallback: fetch
    if (typeof fetch === 'function') {{
      const response = await fetch(request.url, {{
        method: request.method,
        headers: request.headers,
        body: request.method !== 'GET' ? request.body : undefined
      }});
      if (!response.ok) {{
        const text = await response.text().catch(() => '');
        throw new Error(`HTTP ${{response.status}}: ${{text.substring(0, 200)}}`);
      }}
      return response.json();
    }}

    throw new Error('No HTTP client available (need jQuery or fetch)');
  }}
}};

async function buildBrowserEntrypointResult(args, deps = defaultDeps) {{
  // 1. Parameter validation
  const validation = validateArgs(args);
  if (!validation.valid) {{
    const mode = detectMode(args);
    return buildArtifact({{
      status: 'blocked',
      blockedReason: 'validation_failed',
      reasons: validation.errors,
      rows: [],
      args,
      columnDefs: mode.columnDefs,
      columns: mode.columnDefs.map(([key]) => key)
    }});
  }}

  // 2. Page context validation
  const pageValidation = typeof deps.validatePageContext === 'function'
    ? deps.validatePageContext(args)
    : {{ ok: true }};
  if (!pageValidation?.ok) {{
    const mode = detectMode(args);
    return buildArtifact({{
      status: 'blocked',
      blockedReason: pageValidation?.reason || 'page_context_mismatch',
      reasons: [pageValidation?.reason || 'page_context_mismatch'],
      rows: [],
      args,
      columnDefs: mode.columnDefs,
      columns: mode.columnDefs.map(([key]) => key)
    }});
  }}

  // 3. Detect mode
  const mode = detectMode(args);

  // 4. Data fetching
  const reasons = [];
  let rawData = null;
  try {{
    rawData = await (deps.queryModeData ? deps.queryModeData(args, mode) : Promise.resolve([]));
  }} catch (error) {{
    return buildArtifact({{
      status: 'error',
      fatalError: error.message,
      reasons: ['api_query_failed:' + error.message],
      rows: [],
      args,
      columnDefs: mode.columnDefs,
      columns: mode.columnDefs.map(([key]) => key)
    }});
  }}

  // 5. Extract response data
  const responsePath = mode.responsePath || '';
  let data = rawData;
  if (responsePath && rawData) {{
    data = rawData[responsePath] || rawData;
  }}

  // 6. Row normalization
  const rows = normalizeModeRows(data, mode);
  if (rows.length === 0 && Array.isArray(data) && data.length > 0) {{
    reasons.push('row_normalization_partial');
  }}

  // 7. Build artifact
  return buildArtifact({{
    reasons,
    rows,
    args,
    columnDefs: mode.columnDefs,
    columns: mode.columnDefs.map(([key]) => key)
  }});
}}

if (typeof module !== 'undefined') {{
  module.exports = {{ buildBrowserEntrypointResult, normalizePayload, validateArgs, detectMode, buildModeRequest, normalizeModeRows, buildArtifact, determineArtifactStatus, MODES, REPORT_NAME }};
}}

if (typeof args !== 'undefined') {{
  return buildBrowserEntrypointResult(args);
}}
"#, scene_id = scene_id, modes_json = modes_json, default_mode = default_mode, mode_switch_field = mode_switch_field)
}

fn browser_script_test(tool_name: &str, _analysis: &SceneSourceAnalysis) -> String {
    format!(
        "const assert = require('assert');\nconst {{ buildBrowserEntrypointResult }} = require('./{}.js');\n\n(async () => {{\n  const artifact = await buildBrowserEntrypointResult({{\n    org_label: '国网兰州供电公司',\n    org_code: '62401',\n    period_mode: 'month',\n    period_mode_code: '1',\n    period_value: '2026-03',\n    period_payload: '{{\"fdate\":\"2026-03\"}}'\n  }});\n  assert.equal(artifact.type, 'report-artifact');\n  assert.ok(Array.isArray(artifact.column_defs));\n  assert.equal(artifact.rows.length, 1);\n}})().catch((err) => {{\n  console.error(err);\n  process.exit(1);\n}});\n",
        tool_name
    )
}

fn sanitize_ident(value: &str) -> String {
    value
        .chars()
        .map(|ch| {
            if ch.is_ascii_alphanumeric() {
                ch.to_ascii_lowercase()
            } else {
                '_'
            }
        })
        .collect::<String>()
        .split('_')
        .filter(|part| !part.is_empty())
        .collect::<Vec<_>>()
        .join("_")
}
