"""Doctor-checks emitter — generate a JS module of per-table and per-env-var checks.

Every table in schema.js gets a presence check (via `to_regclass`). Every
required env var gets a presence check, with fixable vars wired to existing
fix actions. The generated JS lives at `app/lib/doctor/generated/
checks-from-shape.mjs` and is imported by the doctor engine under the `shape`
category, so renaming a table or env var auto-propagates into the diagnostic
surface without editing Node code.
"""
from __future__ import annotations

from livingcode.types import ShapeModel

# Required env vars the doctor can auto-fix. Keep this aligned with the
# FIX_REGISTRY entries in `app/lib/doctor/fixes/index.mjs`.
FIXABLE_ENV_VARS = {
    "NEXTAUTH_SECRET": {
        "action": "generate_secret",
        "description": "Generate a random NEXTAUTH_SECRET",
    },
    "ENCRYPTION_KEY": {
        "action": "generate_encryption_key",
        "description": "Generate a 32-character ENCRYPTION_KEY",
    },
    "DASHCLAW_API_KEY": {
        "action": "generate_api_key",
        "description": "Generate a new DashClaw API key",
    },
}


def _js_string_array(values: list[str]) -> str:
    """Render a JS string array, one entry per line, with stable ordering."""
    if not values:
        return "[]"
    lines = ["["]
    for v in values:
        lines.append(f"  {_js_string(v)},")
    lines.append("]")
    return "\n".join(lines)


def _js_string(value: str) -> str:
    """Escape a string for a double-quoted JS literal."""
    escaped = (
        value.replace("\\", "\\\\")
        .replace('"', '\\"')
        .replace("\n", "\\n")
        .replace("\r", "\\r")
    )
    return f'"{escaped}"'


def _js_object(obj: dict) -> str:
    """Render a small dict as a JS object literal (one property per line)."""
    if not obj:
        return "{}"
    lines = ["{"]
    for k, v in obj.items():
        lines.append(f"  {_js_string(k)}: {{ action: {_js_string(v['action'])}, description: {_js_string(v['description'])} }},")
    lines.append("}")
    return "\n".join(lines)


def emit_doctor_checks(shape: ShapeModel) -> str:
    """Render the shape-derived doctor checks as an ESM module string."""
    tables = sorted({t.name for t in shape.tables})
    required_env = sorted(e.name for e in shape.env_vars if e.required)

    tables_literal = _js_string_array(tables)
    required_literal = _js_string_array(required_env)
    fixable_literal = _js_object(FIXABLE_ENV_VARS)

    return f"""// app/lib/doctor/generated/checks-from-shape.mjs
// AUTO-GENERATED by livingcode. Do not edit by hand.
// Regenerate with `npm run livingcode:refresh`.

import {{ getSql }} from '../../db.js';
import {{ getSetupStatus }} from '../../setupStatus.mjs';

const TABLES = {tables_literal};
const REQUIRED_ENV_VARS = {required_literal};
const FIXABLE_ENV_VARS = {fixable_literal};

/**
 * Shape-derived doctor checks. Signature matches the other check runners in
 * `app/lib/doctor/checks/*.mjs` so it can be plugged into the engine's
 * CHECK_RUNNERS map under the `shape` category.
 *
 * @param {{ {{ env?: object, host?: string }} }} options
 */
export async function runShapeChecks({{ env = process.env }} = {{}}) {{
  const checks = [];

  // Table presence checks — only if the DB is reachable. Table names come
  // from the committed generated code (schema.js → shape.json), so they are
  // safe to interpolate into a SQL literal (Neon driver has no sql.identifier).
  let sql = null;
  try {{
    const dbStatus = await getSetupStatus(env);
    if (dbStatus.configured) sql = getSql();
  }} catch {{
    // db unreachable — fall through, surfaced elsewhere
  }}

  if (sql && TABLES.length > 0) {{
    // Single round trip: to_regclass each table name inside unnest() so this
    // stays O(1) DB calls regardless of schema size. The tagged template form
    // binds TABLES as a text[] parameter — the Neon serverless driver's
    // `sql.unsafe(rawString)` does NOT execute, it just builds a query object,
    // so the tagged form is required for real results.
    let present = null;
    try {{
      const rows = await sql`
        SELECT t AS name, to_regclass('public.' || t) AS oid
        FROM unnest(${{TABLES}}::text[]) AS t
      `;
      present = new Set(rows.filter((r) => r.oid != null).map((r) => r.name));
    }} catch {{
      // Query-level failure — skip table checks; database check module surfaces it.
    }}

    if (present) {{
      for (const table of TABLES) {{
        const exists = present.has(table);
        checks.push({{
          id: `shape_table_${{table}}`,
          category: 'shape',
          status: exists ? 'pass' : 'fail',
          title: `Table: ${{table}}`,
          message: exists
            ? 'Present'
            : 'Missing from database — run migrations to create it',
          fix: exists
            ? null
            : {{
                type: 'auto',
                description: 'Run database migrations',
                action: 'migrate',
              }},
        }});
      }}
    }}
  }}

  // Required env-var presence checks — fixable vars wire to existing fix
  // actions; everything else directs the operator to set it manually.
  for (const name of REQUIRED_ENV_VARS) {{
    const present = Boolean(env?.[name]);
    const fixInfo = FIXABLE_ENV_VARS[name];
    checks.push({{
      id: `shape_env_${{name}}`,
      category: 'shape',
      status: present ? 'pass' : 'fail',
      title: `Env: ${{name}}`,
      message: present
        ? 'Set'
        : fixInfo
          ? 'Not set — auto-fix available'
          : `Not set — set ${{name}} in the environment (no auto-fix)`,
      fix:
        present || !fixInfo
          ? null
          : {{
              type: 'auto',
              description: fixInfo.description,
              action: fixInfo.action,
            }},
    }});
  }}

  return checks;
}}
"""
