/**
 * materialize.js — Layer 7 inactive draft writer.
 *
 * Writes inactive draft artifact(s) from an approved candidate.
 * First-slice: instinct artifact type only.
 * Does NOT write to active paths (skills/, instincts/, CLAUDE.md).
 * Emits MaterializationRecord + Layer 5 lifecycle transition.
 */

const fs = require('node:fs');
const path = require('node:path');
const os = require('node:os');
const crypto = require('node:crypto');

const { sanitizeFilename, atomicWriteFile, sha256Truncated } = require('../utils');
const { redactObservationText, SANITIZER_POLICY_VERSION } = require('../sanitize-observation');
const { appendTransitionEvent } = require('./dashboard-events');

// ---------------------------------------------------------------------------
// First-slice supported artifact types
// ---------------------------------------------------------------------------

const FIRST_SLICE_SUPPORTED_TYPES = ['instinct'];

// ---------------------------------------------------------------------------
// candidate_record_hash — stable hash over semantic fields
// Fields chosen to be stable: changing any of these means a new materialization.
// ---------------------------------------------------------------------------

function candidateRecordHash(candidate) {
  const stable = {
    candidate_id: candidate.candidate_id,
    body: candidate.body,
    name: candidate.name,
    summary: candidate.summary,
    artifact_type: candidate.artifact_type,
    scope: candidate.scope,
    body_source: candidate.body_source,
    evidence_quality: candidate.evidence_quality,
    updated_at: candidate.updated_at,
  };
  return sha256Truncated(JSON.stringify(stable), 64);
}

// ---------------------------------------------------------------------------
// Path helpers
// ---------------------------------------------------------------------------

/**
 * Returns the draft root for a given candidate + materialization.
 * @param {string} arcforgeRoot
 * @param {string} candidateId
 * @param {string} materializationId
 * @returns {string}
 */
function getDraftRoot(arcforgeRoot, candidateId, materializationId) {
  return path.join(arcforgeRoot, 'learning', 'drafts', candidateId, materializationId);
}

function getDraftsBase(arcforgeRoot) {
  return path.join(arcforgeRoot, 'learning', 'drafts');
}

function getLockPath(arcforgeRoot, candidateId) {
  return path.join(arcforgeRoot, 'learning', 'drafts', candidateId, 'materialization.lock');
}

// ---------------------------------------------------------------------------
// Render policy
// ---------------------------------------------------------------------------

/**
 * Default render policy for first-slice materialization.
 * @returns {object}
 */
function defaultRenderPolicy() {
  return {
    policy_version: 'v1',
    allowed_artifact_types: FIRST_SLICE_SUPPORTED_TYPES,
    draft_root: path.join(os.homedir(), '.arcforge', 'learning', 'drafts'),
    active_roots_forbidden: true,
    overwrite_existing_draft: false,
    atomic_write_required: true,
    include_evidence_summaries: false,
    include_raw_evidence: false,
    include_raw_prompts: false,
    include_raw_transcripts: false,
  };
}

// ---------------------------------------------------------------------------
// Draft content builder
// ---------------------------------------------------------------------------

/**
 * Build draft file content for an instinct candidate.
 * Includes frontmatter metadata + candidate body + inactive-draft warning.
 *
 * @param {{ candidate, materializationId, renderPolicy }} opts
 * @returns {string}
 */
function buildDraftContent({ candidate, materializationId, renderPolicy }) {
  const metadata = {
    schema_version: 1,
    candidate_id: candidate.candidate_id,
    materialization_id: materializationId,
    artifact_type: candidate.artifact_type,
    name: candidate.name,
    summary: candidate.summary,
    body_source: candidate.body_source,
    scope: candidate.scope,
    evidence_quality: candidate.evidence_quality,
    generated_at: new Date().toISOString(),
    render_policy_version: renderPolicy.policy_version,
    inactive_draft: true,
  };

  // Include promoted_from provenance if present
  if (candidate.relationships) {
    if (candidate.relationships.promoted_from_candidate_id) {
      metadata.promoted_from_candidate_id = candidate.relationships.promoted_from_candidate_id;
    }
    if (candidate.relationships.promoted_from_project_id) {
      metadata.promoted_from_project_id = candidate.relationships.promoted_from_project_id;
    }
  }

  const frontmatter = JSON.stringify(metadata, null, 2);

  const lines = [
    '<!-- INACTIVE DRAFT — DO NOT LOAD INTO ACTIVE RUNTIME -->',
    '<!-- This file was generated by Layer 7 materialization and has NOT been activated. -->',
    '',
    '```json',
    frontmatter,
    '```',
    '',
    '---',
    '',
    `# ${candidate.name}`,
    '',
    `> **Summary:** ${candidate.summary}`,
    '',
    '## Body',
    '',
    candidate.body,
    '',
  ];

  if (candidate.rationale) {
    lines.push('## Rationale', '', candidate.rationale, '');
  }

  if (candidate.trigger) {
    lines.push('## Trigger', '', candidate.trigger, '');
  }

  lines.push('---');
  lines.push('');
  lines.push('**⚠ INACTIVE DRAFT**: This artifact has not been activated. It is not loaded into');
  lines.push('any active runtime surface. To activate, use the ArcForge learning dashboard.');

  return lines.join('\n');
}

// ---------------------------------------------------------------------------
// Simple file-based lock (same pattern as queue-writer.js)
// ---------------------------------------------------------------------------

const LOCK_TIMEOUT = 5000;
const LOCK_STALE_THRESHOLD = 30000;

function acquireDraftLock(lockPath) {
  const dir = path.dirname(lockPath);
  fs.mkdirSync(dir, { recursive: true });

  const startTime = Date.now();
  let interval = 50;

  while (true) {
    try {
      const fd = fs.openSync(
        lockPath,
        fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY,
      );
      fs.writeSync(fd, JSON.stringify({ pid: process.pid, ts: new Date().toISOString() }));
      fs.closeSync(fd);
      return;
    } catch (err) {
      if (err.code !== 'EEXIST') throw err;

      try {
        const stat = fs.statSync(lockPath);
        if (Date.now() - stat.mtimeMs > LOCK_STALE_THRESHOLD) {
          try {
            fs.unlinkSync(lockPath);
          } catch {
            /* retry */
          }
          continue;
        }
      } catch {
        continue;
      }

      if (Date.now() - startTime > LOCK_TIMEOUT) {
        throw new Error(`Failed to acquire draft lock after ${LOCK_TIMEOUT}ms: ${lockPath}`);
      }

      const waitMs = Math.min(interval, 500);
      const end = Date.now() + waitMs;
      while (Date.now() < end) {
        /* busy-wait */
      }
      interval = Math.min(interval * 2, 500);
    }
  }
}

function releaseDraftLock(lockPath) {
  try {
    fs.unlinkSync(lockPath);
  } catch (err) {
    if (err.code !== 'ENOENT') throw err;
  }
}

// ---------------------------------------------------------------------------
// Failure logger
// ---------------------------------------------------------------------------

function logFailure(arcforgeRoot, failureRecord) {
  try {
    const failurePath = path.join(getDraftsBase(arcforgeRoot), 'failures.jsonl');
    fs.mkdirSync(path.dirname(failurePath), { recursive: true });
    fs.appendFileSync(failurePath, `${JSON.stringify(failureRecord)}\n`, 'utf8');
  } catch {
    // Failure logging is best-effort; do not propagate
  }
}

function makeFailure(opts) {
  return {
    schema_version: 1,
    failure_id: `matfail_${Date.now()}_${crypto.randomBytes(6).toString('hex')}`,
    failed_at: new Date().toISOString(),
    candidate_id: opts.candidateId,
    source_action_id: opts.sourceActionId,
    reason: opts.reason,
    detail: opts.detail || undefined,
    draft_artifacts_written: false,
    reported_to_layer5: false,
  };
}

// ---------------------------------------------------------------------------
// Idempotence — scan for existing materializations
// ---------------------------------------------------------------------------

function findExistingMaterialization(arcforgeRoot, candidateId, candidateHash, policyVersion) {
  const candidateDraftsDir = path.join(getDraftsBase(arcforgeRoot), candidateId);
  if (!fs.existsSync(candidateDraftsDir)) return null;

  let entries;
  try {
    entries = fs.readdirSync(candidateDraftsDir);
  } catch {
    return null;
  }

  for (const entry of entries) {
    const manifestPath = path.join(candidateDraftsDir, entry, 'materialization.json');
    if (!fs.existsSync(manifestPath)) continue;
    try {
      const record = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
      if (
        record.source_candidate &&
        record.source_candidate.candidate_record_hash === candidateHash &&
        record.render_policy_version === policyVersion
      ) {
        return record;
      }
    } catch {
      // Corrupted manifest — skip
    }
  }
  return null;
}

// ---------------------------------------------------------------------------
// Primary export — materialize()
// ---------------------------------------------------------------------------

/**
 * Materialize an approved candidate into an inactive draft artifact.
 *
 * @param {object} opts
 * @returns {object} `{ ok: true, record, draftPaths }` on success;
 *                   `{ ok: false, failure }` on validation/write failure.
 *                   See layer-7-materialization.md for full record shapes.
 */
function materialize({
  candidate,
  sourceActionId,
  requestedArtifactType,
  reviewerNote: _reviewerNote,
  renderPolicy,
  arcforgeRoot,
}) {
  const effectiveRoot = arcforgeRoot || path.join(os.homedir(), '.arcforge');
  const effectivePolicy = renderPolicy || defaultRenderPolicy();
  const actor = { layer: 7, actor_type: 'materializer' };

  function fail(reason, detail) {
    const failure = makeFailure({
      candidateId: candidate ? candidate.candidate_id : 'unknown',
      sourceActionId: sourceActionId || 'unknown',
      reason,
      detail,
    });
    logFailure(effectiveRoot, failure);
    return { ok: false, failure };
  }

  // L7-1: candidate must be approved (first materialization) or deactivated (re-materialization).
  // Matrix-allowed transitions: approved → materialized, deactivated → materialized.
  const status = candidate?.lifecycle ? candidate.lifecycle.status : undefined;
  if (status !== 'approved' && status !== 'deactivated') {
    return fail('invalid_lifecycle_status', `Expected approved or deactivated, got: ${status}`);
  }

  // L7-2: First-slice supports instinct only
  // Reject if candidate artifact_type is not supported (regardless of requestedArtifactType)
  const artifactType = requestedArtifactType || candidate.artifact_type;
  const candidateArtifactType = candidate.artifact_type;
  if (
    !FIRST_SLICE_SUPPORTED_TYPES.includes(candidateArtifactType) ||
    !FIRST_SLICE_SUPPORTED_TYPES.includes(artifactType)
  ) {
    return fail(
      'artifact_type_mismatch',
      `First-slice supports instinct only; got: ${candidateArtifactType}`,
    );
  }

  // L7-12: Validate name with sanitizeFilename
  let safeName;
  try {
    safeName = sanitizeFilename(candidate.name);
  } catch (err) {
    return fail('path_policy_rejected', `Invalid candidate name: ${err.message}`);
  }

  // L7-3: Secret scan on body
  const sanitizedBody = redactObservationText(candidate.body || '');
  if (sanitizedBody !== (candidate.body || '')) {
    return fail('unsafe_content', 'candidate body failed secret scan');
  }

  // Idempotence check (L7-11)
  const recordHash = candidateRecordHash(candidate);
  const existingRecord = findExistingMaterialization(
    effectiveRoot,
    candidate.candidate_id,
    recordHash,
    effectivePolicy.policy_version,
  );
  if (existingRecord) {
    const existingDraftPaths = existingRecord.draft_artifacts.map((a) => a.draft_path);
    return { ok: true, record: existingRecord, draftPaths: existingDraftPaths };
  }

  // Allocate IDs
  const materializationId = `mat_${Date.now()}_${crypto.randomBytes(6).toString('hex')}`;
  const draftRootDir = getDraftRoot(effectiveRoot, candidate.candidate_id, materializationId);
  const lockPath = getLockPath(effectiveRoot, candidate.candidate_id);

  // Acquire draft lock
  try {
    acquireDraftLock(lockPath);
  } catch (err) {
    return fail('lock_timeout', err.message);
  }

  try {
    // Build draft content
    const draftContent = buildDraftContent({
      candidate,
      materializationId,
      renderPolicy: effectivePolicy,
    });

    // L7-4: Draft path
    const instinctsDir = path.join(draftRootDir, 'instincts');
    const draftFileName = `${safeName}.md`;
    const draftFilePath = path.join(instinctsDir, draftFileName);

    // L7-12: Verify path containment
    const resolvedDraft = path.resolve(draftFilePath);
    const resolvedRoot = path.resolve(draftRootDir);
    if (!resolvedDraft.startsWith(resolvedRoot + path.sep) && resolvedDraft !== resolvedRoot) {
      return fail('path_policy_rejected', 'Draft path escapes draft root');
    }

    // L7-4 / L7-7: Atomic write draft file, then manifest
    atomicWriteFile(draftFilePath, draftContent);

    // L7-10: Compute content hash after write
    const onDiskContent = fs.readFileSync(draftFilePath, 'utf8');
    const contentHash = sha256Truncated(onDiskContent, 64);
    const draftArtifactId = `dart_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;

    // Build MaterializationRecord
    const materializationRecord = {
      schema_version: 1,
      materialization_id: materializationId,
      created_at: new Date().toISOString(),
      candidate_id: candidate.candidate_id,
      source_action_id: sourceActionId,
      source_candidate: {
        candidate_id: candidate.candidate_id,
        candidate_record_hash: recordHash,
        lifecycle_status_at_materialization: status,
      },
      artifact_type: artifactType,
      draft_artifacts: [
        {
          draft_artifact_id: draftArtifactId,
          artifact_role: 'primary',
          draft_path: draftFilePath,
          draft_path_hash: sha256Truncated(draftFilePath, 32),
          active_target_hint: {
            target_kind: 'instinct',
            target_path_summary: `instincts/${candidate.scope && candidate.scope.kind === 'global' ? 'global' : (candidate.scope?.project_id) || 'project'}/${candidate.candidate_id}.md`,
            auto_apply_allowed: false,
          },
          content_hash: contentHash,
          bytes: Buffer.byteLength(onDiskContent, 'utf8'),
          created_at: new Date().toISOString(),
        },
      ],
      render_policy_version: effectivePolicy.policy_version,
      safety: {
        active_paths_written: false,
        draft_only: true,
        raw_evidence_included: false,
        raw_prompt_included: false,
        raw_response_included: false,
        raw_transcript_included: false,
        skill_args_included: false,
        secret_scan: { status: 'passed', rule_version: SANITIZER_POLICY_VERSION },
        path_policy: {
          status: 'passed',
          draft_root: draftRootDir,
          active_roots_forbidden: true,
        },
        content_hash_algorithm: 'sha256',
      },
      reported_to_layer5: false,
    };

    // L7-7: Write materialization.json atomically AFTER draft file
    const manifestPath = path.join(draftRootDir, 'materialization.json');
    atomicWriteFile(manifestPath, `${JSON.stringify(materializationRecord, null, 2)}\n`);

    // L7-8: Report to Layer 5 AFTER manifest is durable
    materializationRecord.reported_to_layer5 = true;
    appendTransitionEvent(candidate.candidate_id, 'materialize', 'materialized', actor);

    return {
      ok: true,
      record: materializationRecord,
      draftPaths: [draftFilePath],
    };
  } catch (err) {
    return fail('write_failed', err.message);
  } finally {
    releaseDraftLock(lockPath);
  }
}

module.exports = {
  materialize,
  buildDraftContent,
  getDraftRoot,
  defaultRenderPolicy,
};
