#!/bin/bash
# =============================================================================
# run_eda.sh — EDA Flow Orchestrator (Two-Pass or Basic Yosys Synthesis)
# =============================================================================
#
# Orchestrates the full EDA flow: Yosys synthesis → OpenROAD PnR → OpenSTA → metrics.
#
# Synthesis Mode (SYNTH_MODE env var):
#   "twopass" (default) — Two-pass timing-driven synthesis:
#     Pass 1: Area-balanced synthesis (fast, compact)
#     STA 1:  Timing analysis — if timing is met, done
#     Pass 2: Timing-recovery re-mapping on the Pass 1 netlist
#             Uses ABC 'map' command with gate sizing (upsize/dnsize)
#     STA 2:  Timing analysis — compare with Pass 1, pick best
#     If Pass 2a doesn't improve, falls back to full re-synthesis from RTL.
#
#   "basic" — Original single-pass synthesis (backward compatible)
#
# Constraint Entry Points:
#   1. STA_SDC_FILE — User-supplied SDC file (used directly)
#   2. CLOCK_NAME/CLOCK_NS/IDELAY_NS/ODELAY_NS — Env vars → auto-generated SDC
#      Defaults: IDELAY_NS=2, ODELAY_NS=2
#
# Clock Name Resolution (when no STA_SDC_FILE):
#   1. CLOCK_NAME env var — if set by user, used directly.
#   2. Auto-detect — clock_ident.py scans RTL source files for clock ports
#      using naming heuristics (clk, clock, pclk, hclk, aclk, etc.)
#   3. Fallback: "clock" (if auto-detection finds no clock ports)
#
# Clock Period Resolution:
#   1. CLOCK_NS env var — if set by user, used for all stages.
#   2. PDK default — if CLOCK_NS is unset, a PDK-appropriate period is used
#      based on ~15 logic levels of NAND2 delay (standard datapath proxy).
#   3. Adaptive (opt-in) — ADAPTIVE_CLOCK=1 enables adaptive mode:
#      Pass 1 uses relaxed 10ns, then tightens to 95% of worst_path_delay
#      after STA.  Pass 2 + PnR use the tight clock.
#      Has no effect when CLOCK_NS is explicitly set.
#   4. User-supplied SDC overrides all clock period logic.
#
# =============================================================================

mkdir -p ${OUT_DIR}

# ─────────────────────────────────────────────────────────────────────────────
# Utility Functions
# ─────────────────────────────────────────────────────────────────────────────

# Convert seconds to HH:MM:SS format
format_time() {
  local seconds=$1
  printf '%02d:%02d:%02d' $((seconds/3600)) $((seconds%3600/60)) $((seconds%60))
}

# awk-based float comparison: returns 0 (true) if $1 >= $2
float_gte() {
  awk "BEGIN{exit(!($1 >= $2))}"
}

# awk-based float comparison: returns 0 (true) if $1 > $2
float_gt() {
  awk "BEGIN{exit(!($1 > $2))}"
}

# awk-based float multiplication: prints $1 * $2 * $3
float_mul() {
  awk "BEGIN{printf \"%d\", $1 * $2 * $3}"
}

# awk-based: prints $1 * $2 with 4 decimal places
float_mul2() {
  awk "BEGIN{printf \"%.4f\", $1 * $2}"
}

# Parse a key=value from a metrics file
parse_metric() {
  local metrics_file="$1"
  local key="$2"
  grep "^${key}=" "$metrics_file" | cut -d= -f2
}

# Generate an SDC constraints file from current CLOCK_NAME, CLOCK_NS, etc.
# Usage: generate_sdc <output_file>
generate_sdc() {
  local sdc_file="$1"
  cat > "${sdc_file}" <<SDCEOF
# Auto-generated SDC constraints
# Generated by run_eda.sh from env vars:
#   CLOCK_NAME=${CLOCK_NAME}, CLOCK_NS=${CLOCK_NS}
#   IDELAY_NS=${IDELAY_NS}, ODELAY_NS=${ODELAY_NS}

create_clock -period ${CLOCK_NS} -name ${CLOCK_NAME} [get_ports ${CLOCK_NAME}]
set_input_delay -clock [get_clocks ${CLOCK_NAME}] ${IDELAY_NS} [all_inputs]
set_output_delay -clock [get_clocks ${CLOCK_NAME}] ${ODELAY_NS} [all_outputs]
SDCEOF
}

# Extract congestion metrics from the flow log for a given stage and inject
# into the stage's metrics .txt and .json files.
# Usage: inject_congestion <stage_name> <flow_log> <out_dir>
inject_congestion() {
  local stage="$1"
  local flow_log="$2"
  local out_dir="$3"
  local stage_txt="${out_dir}/metrics_${stage}.txt"
  local stage_json="${out_dir}/metrics_${stage}.json"

  local start_marker=">>> REPORT_CONGESTION_START ${stage}"
  local end_marker="<<< REPORT_CONGESTION_END ${stage}"

  if ! grep -q "${start_marker}" "${flow_log}"; then
    return 0
  fi

  # Extract the LAST matching congestion section from the log (in case of
  # placement retries, the log may contain multiple sections for the same stage).
  local cong_section
  cong_section=$(tac "${flow_log}" | sed -n "/${end_marker}/,/${start_marker}/p" | tac)

  # Total row: "Total   9796   1689   17.24%   0 /  0 /  0"
  local total_line
  total_line=$(echo "${cong_section}" | grep "^Total ")
  if [ -z "${total_line}" ]; then
    return 0
  fi

  local cong_resource cong_demand cong_overflow cong_wirelength
  cong_resource=$(echo "${total_line}" | awk '{print $2}')
  cong_demand=$(echo "${total_line}" | awk '{print $3}')
  cong_overflow=$(echo "${total_line}" | awk -F'/' '{gsub(/[[:space:]]/,"",$NF); print $NF}')

  # Wirelength: "[INFO GRT-0018] Total wirelength: 18475 um"
  local wl_line
  wl_line=$(echo "${cong_section}" | grep "GRT-0018")
  if [ -n "${wl_line}" ]; then
    cong_wirelength=$(echo "${wl_line}" | sed 's/.*wirelength: *\([0-9]*\).*/\1/')
  fi

  echo "  ${stage} congestion: resource=${cong_resource:-?} demand=${cong_demand:-?} overflow=${cong_overflow:-?} wirelength=${cong_wirelength:-?}um"

  # Update flat txt file (replace placeholders)
  if [ -f "${stage_txt}" ]; then
    sed -i.bak \
      -e "s/^CONGESTION_RESOURCE=.*/CONGESTION_RESOURCE=${cong_resource:-null}/" \
      -e "s/^CONGESTION_DEMAND=.*/CONGESTION_DEMAND=${cong_demand:-null}/" \
      -e "s/^CONGESTION_OVERFLOW=.*/CONGESTION_OVERFLOW=${cong_overflow:-null}/" \
      -e "s/^CONGESTION_WIRELENGTH_UM=.*/CONGESTION_WIRELENGTH_UM=${cong_wirelength:-null}/" \
      "${stage_txt}"
    rm -f "${stage_txt}.bak"
  fi

  # Update JSON file: replace "congestion": null with actual data
  if [ -f "${stage_json}" ] && [ -n "${cong_resource}" ]; then
    # Build per-layer JSON array from the GRT-0096 final congestion table.
    local layer_json
    layer_json=$(echo "${cong_section}" | awk '
      /Final congestion report/ { in_table = 1; next }
      !in_table { next }
      /^-+$/ { next }
      /^Layer / { next }
      /^Total / { in_table = 0; next }
      /^[[:space:]]*$/ { in_table = 0; next }
      {
        name = $1; res = $2; dem = $3; use = $4
        n = split($0, slash, "/")
        gsub(/[[:space:]]/, "", slash[n])
        ov = slash[n]
        gsub(/%/, "", use)
        if (cnt++ > 0) printf ","
        printf "{\"name\":\"%s\",\"resource\":%s,\"demand\":%s,\"usage_pct\":%s,\"overflow\":%s}", name, res, dem, use, ov
      }
    ')

    local cong_tmp="${out_dir}/_congestion_blob_${stage}.json"
    printf '{"layers":[%s],"total_resource":%s,"total_demand":%s,"total_overflow":%s,"total_wirelength_um":%s}' \
      "${layer_json}" "${cong_resource}" "${cong_demand}" "${cong_overflow}" "${cong_wirelength:-0}" \
      > "${cong_tmp}"

    python3 -c "
import json, sys
with open(sys.argv[1], 'r') as f:
    data = json.load(f)
with open(sys.argv[2], 'r') as f:
    data['congestion'] = json.load(f)
with open(sys.argv[1], 'w') as f:
    json.dump(data, f, indent=2)
" "${stage_json}" "${cong_tmp}" 2>&1 || echo "WARNING: Could not inject ${stage} congestion into JSON (non-fatal)"
    rm -f "${cong_tmp}"
  fi
}

# ─────────────────────────────────────────────────────────────────────────────
# Defaults for env vars
# ─────────────────────────────────────────────────────────────────────────────
SYNTH_MODE="${SYNTH_MODE:-twopass}"
SKIP_PASS2="${SKIP_PASS2:-0}"
STOP_STAGE="${STOP_STAGE:-route}"
PLATFORM="${PLATFORM:-sky130hs}"
IDELAY_NS="${IDELAY_NS:-2}"
ODELAY_NS="${ODELAY_NS:-2}"

# ─────────────────────────────────────────────────────────────────────────────
# PDK Library File Resolution
# ─────────────────────────────────────────────────────────────────────────────
# Resolves LIBERTY_FILE, LEF_TECH_FILE, LEF_FILE from PLATFORM when not
# user-supplied.  Also sets:
#   LIBERTY_FILE_DFF — Single lib for Yosys dfflibmap (flip-flop mapping)
#   LIBERTY_FILE_ABC — Single lib for Yosys abc (combinational mapping)
# For single-lib PDKs these are the same as LIBERTY_FILE.
# For multi-lib PDKs (ASAP7) these select the appropriate sub-library.

_ORFS_BASE="/tools/OpenROAD-flow-scripts/flow/platforms"

resolve_pdk_libs() {
  local platform="$1"
  local pdir="${_ORFS_BASE}/${platform}"

  case "${platform}" in
    sky130hs)
      LIBERTY_FILE="${pdir}/lib/sky130_fd_sc_hs__tt_025C_1v80.lib"
      LEF_TECH_FILE="${pdir}/lef/sky130_fd_sc_hs.tlef"
      LEF_FILE="${pdir}/lef/sky130_fd_sc_hs_merged.lef"
      LIBERTY_FILE_DFF="${LIBERTY_FILE}"
      LIBERTY_FILE_ABC="${LIBERTY_FILE}"
      ;;
    sky130hd)
      LIBERTY_FILE="${pdir}/lib/sky130_fd_sc_hd__tt_025C_1v80.lib"
      LEF_TECH_FILE="${pdir}/lef/sky130_fd_sc_hd.tlef"
      LEF_FILE="${pdir}/lef/sky130_fd_sc_hd_merged.lef"
      LIBERTY_FILE_DFF="${LIBERTY_FILE}"
      LIBERTY_FILE_ABC="${LIBERTY_FILE}"
      ;;
    nangate45)
      LIBERTY_FILE="${pdir}/lib/NangateOpenCellLibrary_typical.lib"
      LEF_TECH_FILE="${pdir}/lef/NangateOpenCellLibrary.tech.lef"
      # Use .macro.mod.lef which includes TAPCELL_X1 (missing from base .macro.lef)
      LEF_FILE="${pdir}/lef/NangateOpenCellLibrary.macro.mod.lef"
      LIBERTY_FILE_DFF="${LIBERTY_FILE}"
      LIBERTY_FILE_ABC="${LIBERTY_FILE}"
      ;;
    asap7)
      # ASAP7 has 5 sub-libraries (RVT TT NLDM corner).
      # All 5 must be read by OpenSTA/OpenROAD for complete cell coverage.
      # For Yosys:
      #   dfflibmap needs the SEQ library (flip-flops)
      #   abc needs the SIMPLE library (combinational cells)
      local libdir="${pdir}/lib/NLDM"
      local lib_ao="${libdir}/asap7sc7p5t_AO_RVT_TT_nldm_211120.lib.gz"
      local lib_invbuf="${libdir}/asap7sc7p5t_INVBUF_RVT_TT_nldm_220122.lib.gz"
      local lib_oa="${libdir}/asap7sc7p5t_OA_RVT_TT_nldm_211120.lib.gz"
      local lib_seq="${libdir}/asap7sc7p5t_SEQ_RVT_TT_nldm_220123.lib"
      local lib_simple="${libdir}/asap7sc7p5t_SIMPLE_RVT_TT_nldm_211120.lib.gz"
      LIBERTY_FILE="${lib_ao} ${lib_invbuf} ${lib_oa} ${lib_seq} ${lib_simple}"
      LEF_TECH_FILE="${pdir}/lef/asap7_tech_1x_201209.lef"
      LEF_FILE="${pdir}/lef/asap7sc7p5t_28_R_1x_220121a.lef"
      LIBERTY_FILE_DFF="${lib_seq}"
      LIBERTY_FILE_ABC="${lib_simple}"
      ;;
    gf180)
      LIBERTY_FILE="${pdir}/lib/sg13g2_stdcell_typ_1p20V_25C.lib"
      LEF_TECH_FILE="${pdir}/lef/sg13g2_tech.lef"
      LEF_FILE="${pdir}/lef/sg13g2_stdcell.lef"
      LIBERTY_FILE_DFF="${LIBERTY_FILE}"
      LIBERTY_FILE_ABC="${LIBERTY_FILE}"
      ;;
    ihp-sg13g2)
      LIBERTY_FILE="${pdir}/lib/sg13g2_stdcell_typ_1p20V_25C.lib"
      LEF_TECH_FILE="${pdir}/lef/sg13g2_tech.lef"
      LEF_FILE="${pdir}/lef/sg13g2_stdcell.lef"
      LIBERTY_FILE_DFF="${LIBERTY_FILE}"
      LIBERTY_FILE_ABC="${LIBERTY_FILE}"
      ;;
    *)
      echo "ERROR: Unknown PLATFORM '${platform}' for library resolution."
      echo "Supported: asap7, sky130hs, sky130hd, nangate45, gf180, ihp-sg13g2"
      exit 1
      ;;
  esac

  export LIBERTY_FILE LEF_TECH_FILE LEF_FILE LIBERTY_FILE_DFF LIBERTY_FILE_ABC
}

# Resolve PDK libs for any missing paths.
# If none of LIBERTY_FILE, LEF_TECH_FILE, LEF_FILE are set, resolve all from PDK.
# If some are set (partial override), resolve the missing ones from PDK,
# keeping the user-supplied values.
if [ -z "${LIBERTY_FILE:-}" ] || [ -z "${LEF_TECH_FILE:-}" ] || [ -z "${LEF_FILE:-}" ]; then
  # Save any user-supplied values before resolving
  _user_liberty="${LIBERTY_FILE:-}"
  _user_lef_tech="${LEF_TECH_FILE:-}"
  _user_lef="${LEF_FILE:-}"

  resolve_pdk_libs "${PLATFORM}"

  # Restore user-supplied values (override PDK defaults)
  if [ -n "${_user_liberty}" ]; then
    LIBERTY_FILE="${_user_liberty}"
    # For user-supplied libs, derive DFF/ABC from the first one
    LIBERTY_FILE_DFF="$(echo "${LIBERTY_FILE}" | awk '{print $1}')"
    LIBERTY_FILE_ABC="$(echo "${LIBERTY_FILE}" | awk '{print $1}')"
    export LIBERTY_FILE LIBERTY_FILE_DFF LIBERTY_FILE_ABC
  fi
  if [ -n "${_user_lef_tech}" ]; then
    LEF_TECH_FILE="${_user_lef_tech}"
    export LEF_TECH_FILE
  fi
  if [ -n "${_user_lef}" ]; then
    LEF_FILE="${_user_lef}"
    export LEF_FILE
  fi

  echo "PDK library files resolved from PLATFORM=${PLATFORM}"
else
  # User supplied all three — derive DFF/ABC from the first lib
  LIBERTY_FILE_DFF="${LIBERTY_FILE_DFF:-$(echo "${LIBERTY_FILE}" | awk '{print $1}')}"
  LIBERTY_FILE_ABC="${LIBERTY_FILE_ABC:-$(echo "${LIBERTY_FILE}" | awk '{print $1}')}"
  export LIBERTY_FILE_DFF LIBERTY_FILE_ABC
  echo "Using user-supplied library files"
fi

echo "  LIBERTY_FILE:     ${LIBERTY_FILE}"
echo "  LEF_TECH_FILE:    ${LEF_TECH_FILE}"
echo "  LEF_FILE:         ${LEF_FILE}"
echo "  LIBERTY_FILE_DFF: ${LIBERTY_FILE_DFF}"
echo "  LIBERTY_FILE_ABC: ${LIBERTY_FILE_ABC}"

# ─────────────────────────────────────────────────────────────────────────────
# Clock Period Resolution
# ─────────────────────────────────────────────────────────────────────────────
# Three modes:
#   CLOCK_NS_MODE="user"        — user specified --clock-ns=X
#   CLOCK_NS_MODE="pdk_default" — no clock specified, PDK lookup table used
#   CLOCK_NS_MODE="adaptive"    — --adaptive-clock=1, no --clock-ns:
#                                  Pass 1 @ 10ns relaxed, tighten after STA
#
# PDK default clock periods are sized for a standard datapath scenario:
# approximately 15 levels of NAND2 logic depth, derived from ORFS reference
# design constraints (GCD and AES benchmarks).

# PDK default clock period lookup (ns)
_pdk_default_clock_ns() {
  case "$1" in
    sky130hs)   echo "2.0" ;;   # ~15 FO4; between ORFS GCD(1.4) and AES(2.8)
    sky130hd)   echo "1.6" ;;   # HD cells ~20% faster than HS
    nangate45)  echo "0.7" ;;   # between ORFS GCD(0.46) and AES(0.82)
    asap7)      echo "0.5" ;;   # predictive 7nm; ORFS GCD uses 0.31
    gf180)      echo "3.5" ;;   # 180nm, slowest; ORFS AES uses 3.0
    ihp-sg13g2) echo "3.0" ;;   # 130nm IHP; ORFS GCD uses 2.8
    *)          echo "2.0" ;;   # fallback: conservative
  esac
}

CLOCK_NS_AUTO=0
CLOCK_NS_RELAXED=""
ADAPTIVE_CLOCK="${ADAPTIVE_CLOCK:-0}"

if [ -n "${CLOCK_NS:-}" ]; then
  # User explicitly set clock period — use it, ignore adaptive flag
  CLOCK_NS_MODE="user"
  echo "Clock period: ${CLOCK_NS}ns (user-specified)"
elif [ "${ADAPTIVE_CLOCK}" = "1" ]; then
  # Adaptive mode: relaxed clock for Pass 1, tighten after STA
  CLOCK_NS_AUTO=1
  CLOCK_NS_MODE="adaptive"
  CLOCK_NS="10"
  CLOCK_NS_RELAXED="10"
  echo "Clock period: adaptive (Pass 1 @ ${CLOCK_NS}ns relaxed, will tighten after STA)"
else
  # PDK default: fixed period appropriate for standard datapath
  CLOCK_NS_MODE="pdk_default"
  CLOCK_NS=$(_pdk_default_clock_ns "${PLATFORM}")
  echo "Clock period: ${CLOCK_NS}ns (PDK default for ${PLATFORM}, ~15 logic levels)"
fi
export CLOCK_NS

# Unpack VERILOG_FILES which may be a comma delimited string, into space delimited:
if [ -n "${VERILOG_FILES:-}" ]; then
  VERILOG_FILES="${VERILOG_FILES//,/ }"
fi

# ─────────────────────────────────────────────────────────────────────────────
# Clock Name Resolution
# ─────────────────────────────────────────────────────────────────────────────
# Priority:
#   1. User-supplied SDC (STA_SDC_FILE) — clock is defined there, skip detection.
#   2. User-supplied CLOCK_NAME env var — use directly.
#   3. Auto-detect from Verilog source files using clock_ident.py.
#   4. Fallback: "clock" (historic default).

if [ -n "${STA_SDC_FILE:-}" ] && [ -f "${STA_SDC_FILE}" ]; then
  # User supplied SDC — clock name doesn't matter for SDC generation,
  # but we still need it for synthesis summary. Use env var or default.
  CLOCK_NAME="${CLOCK_NAME:-clock}"
elif [ -n "${CLOCK_NAME:-}" ]; then
  # User explicitly set CLOCK_NAME — use it.
  echo "Clock name: ${CLOCK_NAME} (from env)"
else
  # Auto-detect clock from RTL source files.
  # Extract Verilog file paths from the flist (lines starting with /)
  CLOCK_NAME=""
  FLIST_FILE="eda_flist.yosys.read_slang.f"
  if [ -f "${FLIST_FILE}" ] && [ -f "./clock_ident.py" ]; then
    # Build a list of source files from the flist
    SRC_FILES=$(grep '^/' "${FLIST_FILE}" || true)
    if [ -n "${SRC_FILES}" ]; then
      # Run clock_ident.py on each source file, take the first (primary) clock found
      # Use --quiet mode: prints one clock port name per line, primary first
      for src in ${SRC_FILES}; do
        if [ -f "${src}" ]; then
          DETECTED=$(python3 ./clock_ident.py "${src}" --quiet 2>/dev/null | head -1)
          if [ -n "${DETECTED}" ]; then
            CLOCK_NAME="${DETECTED}"
            echo "Clock name: ${CLOCK_NAME} (auto-detected from ${src})"
            break
          fi
        fi
      done
    fi
  fi

  # Fallback if auto-detection found nothing
  if [ -z "${CLOCK_NAME}" ]; then
    CLOCK_NAME="clock"
    echo "Clock name: ${CLOCK_NAME} (fallback default — auto-detection found no clock ports)"
  fi
fi

# Export CLOCK_NAME so child processes (sta.tcl, sta_analyze.tcl) can read it
export CLOCK_NAME

# ─────────────────────────────────────────────────────────────────────────────
# SDC Generation / Resolution
# ─────────────────────────────────────────────────────────────────────────────
# Always produce an SDC_FILE path for downstream scripts.
# For adaptive clock mode, this initial SDC uses the relaxed clock period.
# It will be regenerated after Pass 1 STA with the tight clock.
if [ -n "${STA_SDC_FILE:-}" ] && [ -f "${STA_SDC_FILE}" ]; then
  # User-supplied SDC — use it directly.
  # Disable adaptive clock when user supplies their own SDC.
  CLOCK_NS_AUTO=0
  CLOCK_NS_MODE="user"
  export SDC_FILE="${STA_SDC_FILE}"
  echo "Using user-supplied SDC: ${SDC_FILE}"
else
  SDC_FILE="${OUT_DIR}/constraints.sdc"
  generate_sdc "${SDC_FILE}"
  export SDC_FILE
  echo "Generated SDC: ${SDC_FILE} (clock=${CLOCK_NS}ns)"
fi

# ─────────────────────────────────────────────────────────────────────────────
# Initialize status tracking
# ─────────────────────────────────────────────────────────────────────────────
YOSYS_STATUS=0
OPENROAD_STATUS=0
OPENSTA_STATUS=0
PYTHON_STATUS=0
YOSYS_TIME=0
OPENROAD_TIME=0
OPENSTA_TIME=0
PYTHON_TIME=0

# Main flow log
FLOW_LOG="${OUT_DIR}/pnr_${FILE_SUFFIX}.log"

echo "Starting EDA flow (SYNTH_MODE=${SYNTH_MODE}, STOP_STAGE=${STOP_STAGE}, PLATFORM=${PLATFORM})" > ${FLOW_LOG}
echo "Top module: ${TOP_MODULE}" >> ${FLOW_LOG}
echo "File suffix: ${FILE_SUFFIX}" >> ${FLOW_LOG}

# =============================================================================
# SYNTHESIS PHASE
# =============================================================================

if [ "${SYNTH_MODE}" = "basic" ]; then
  # ─────────────────────────────────────────────────────────────────────────
  # BASIC MODE: Original single-pass synthesis (backward compatible)
  # ─────────────────────────────────────────────────────────────────────────
  echo "Starting Yosys synthesis (basic mode)..."
  YOSYS_START=$(date +%s)
  yosys -m slang -c ./syn_basic.tcl >> ${FLOW_LOG} 2>&1
  YOSYS_STATUS=$?
  YOSYS_END=$(date +%s)
  YOSYS_TIME=$((YOSYS_END - YOSYS_START))

  if [ $YOSYS_STATUS -eq 0 ]; then
    echo "Yosys synthesis completed successfully (walltime: $(format_time $YOSYS_TIME))"
  else
    echo "Yosys synthesis FAILED with exit code: $YOSYS_STATUS (walltime: $(format_time $YOSYS_TIME))"
  fi

else
  # ─────────────────────────────────────────────────────────────────────────
  # TWO-PASS MODE: Timing-driven synthesis
  # ─────────────────────────────────────────────────────────────────────────

  echo ""
  echo "========================================================"
  echo "TWO-PASS TIMING-DRIVEN SYNTHESIS"
  echo "========================================================"
  echo "Top Module:      ${TOP_MODULE}"
  if [ "${CLOCK_NS_MODE}" = "adaptive" ]; then
    echo "Clock:           ${CLOCK_NAME} @ ${CLOCK_NS} ns (adaptive — will tighten after Pass 1)"
  elif [ "${CLOCK_NS_MODE}" = "pdk_default" ]; then
    echo "Clock:           ${CLOCK_NAME} @ ${CLOCK_NS} ns (PDK default for ${PLATFORM})"
  else
    echo "Clock:           ${CLOCK_NAME} @ ${CLOCK_NS} ns"
  fi
  echo "SDC File:        ${SDC_FILE}"
  echo "========================================================"
  echo ""

  # ── Compute delay target ──
  if [ -z "${DELAY_TARGET_PS:-}" ]; then
    DELAY_TARGET_PS=$(float_mul "${CLOCK_NS}" 1000 0.85)
  fi
  export DELAY_TARGET_PS
  echo "Delay Target: ${DELAY_TARGET_PS} ps"

  # ── Auto-detect driving cell from liberty file ──
  # Use LIBERTY_FILE_ABC (single file) for grepping cell names.
  # For multi-lib PDKs, INVBUF lib has buffers but ABC lib (SIMPLE) also
  # contains buffer cells. We search ABC first, then fall back to all libs.
  DRIVING_CELL=""
  _find_cell() {
    local pattern="$1"
    local libfile="${2:-${LIBERTY_FILE_ABC}}"
    grep "^[[:space:]]*cell[[:space:]]*(.*${pattern}" "${libfile}" \
      | head -1 \
      | sed 's/.*cell[[:space:]]*(//; s/").*//; s/).*//; s/"//g; s/[[:space:]]//g'
  }

  for suffix in '_buf_2' '_buf_1' '__buf_2' '__buf_1' '_clkbuf_1' 'BUF_X2' 'BUF_X1' 'buf_X2' 'buf_X1'; do
    match=$(_find_cell "$suffix")
    if [ -n "$match" ]; then
      DRIVING_CELL="$match"
      break
    fi
  done

  # Broader fallback: any cell with 'buf' in name
  if [ -z "$DRIVING_CELL" ]; then
    DRIVING_CELL=$(_find_cell '[Bb][Uu][Ff]')
  fi

  # For multi-lib PDKs, search all liberty files if not found in ABC lib
  if [ -z "$DRIVING_CELL" ] && [ "${LIBERTY_FILE}" != "${LIBERTY_FILE_ABC}" ]; then
    for _lib in ${LIBERTY_FILE}; do
      for suffix in '_buf_2' '_buf_1' '__buf_2' '__buf_1' 'BUFx2' 'BUFx4' 'BUF_X2' 'BUF_X1'; do
        match=$(_find_cell "$suffix" "$_lib")
        if [ -n "$match" ]; then
          DRIVING_CELL="$match"
          break 2
        fi
      done
    done
    # Broader fallback across all libs
    if [ -z "$DRIVING_CELL" ]; then
      for _lib in ${LIBERTY_FILE}; do
        match=$(_find_cell '[Bb][Uu][Ff]' "$_lib")
        if [ -n "$match" ]; then
          DRIVING_CELL="$match"
          break
        fi
      done
    fi
  fi

  if [ -z "$DRIVING_CELL" ]; then
    echo "Warning: Could not auto-detect buffer cell from liberty. Using placeholder."
    DRIVING_CELL="buf_1"
  fi
  echo "Driving Cell: ${DRIVING_CELL} (auto-detected)"

  # ── Generate ABC constraints file ──
  CONSTR_FILE="${OUT_DIR}/abc_constraints.constr"
  cat > "${CONSTR_FILE}" <<CONSTREOF
set_driving_cell ${DRIVING_CELL}
set_load 15.0
CONSTREOF
  export CONSTR_FILE
  echo "ABC Constraints: ${CONSTR_FILE}"
  echo ""

  # ── PASS 1: Area-Balanced Synthesis ──
  echo "── PASS 1: Area-Balanced Synthesis ──────────────────────"
  YOSYS_START=$(date +%s)

  yosys -m slang -c ./synth_pass1.tcl > ${OUT_DIR}/pass1_synth.log 2>&1
  PASS1_SYNTH_STATUS=$?
  PASS1_SYNTH_END=$(date +%s)
  PASS1_SYNTH_TIME=$((PASS1_SYNTH_END - YOSYS_START))

  # Append pass1 synth log to main flow log
  cat ${OUT_DIR}/pass1_synth.log >> ${FLOW_LOG}

  if [ $PASS1_SYNTH_STATUS -ne 0 ]; then
    echo "FATAL: Pass 1 synthesis failed (exit code: $PASS1_SYNTH_STATUS). See ${OUT_DIR}/pass1_synth.log"
    YOSYS_STATUS=$PASS1_SYNTH_STATUS
    YOSYS_END=$(date +%s)
    YOSYS_TIME=$((YOSYS_END - YOSYS_START))
  else
    echo "  Synthesis: ${PASS1_SYNTH_TIME}s"

    PASS1_NETLIST="${OUT_DIR}/${TOP_MODULE}_pass1.syn.v"
    if [ ! -f "${PASS1_NETLIST}" ]; then
      echo "FATAL: Pass 1 netlist not found at: ${PASS1_NETLIST}"
      YOSYS_STATUS=1
      YOSYS_END=$(date +%s)
      YOSYS_TIME=$((YOSYS_END - YOSYS_START))
    else
      export PASS1_NETLIST

      # ── STA 1: Check Pass 1 timing ──
      export NETLIST_FILE="${PASS1_NETLIST}"
      export PASS_NAME="pass1"

      sta -exit ./sta_analyze.tcl > ${OUT_DIR}/pass1_sta.log 2>&1
      PASS1_STA_STATUS=$?

      # Append STA log to main flow log
      cat ${OUT_DIR}/pass1_sta.log >> ${FLOW_LOG}

      PASS1_STA_END=$(date +%s)
      PASS1_STA_TIME=$((PASS1_STA_END - PASS1_SYNTH_END))
      echo "  STA:       ${PASS1_STA_TIME}s"

      if [ $PASS1_STA_STATUS -ne 0 ]; then
        echo "WARNING: Pass 1 STA failed (exit code: $PASS1_STA_STATUS). Proceeding with Pass 1 netlist."
        # STA failure is non-fatal for synthesis selection; we still have a netlist
        BEST_PASS="pass1"
        BEST_NETLIST="${PASS1_NETLIST}"
        BEST_WNS="unknown"
      else
        PASS1_WNS=$(parse_metric "${OUT_DIR}/pass1_metrics.txt" "WNS")
        PASS1_TNS=$(parse_metric "${OUT_DIR}/pass1_metrics.txt" "TNS")
        PASS1_FAILING=$(parse_metric "${OUT_DIR}/pass1_metrics.txt" "FAILING_ENDPOINTS")
        PASS1_TOTAL=$(parse_metric "${OUT_DIR}/pass1_metrics.txt" "TOTAL_ENDPOINTS")
        PASS1_WORST_DELAY=$(parse_metric "${OUT_DIR}/pass1_metrics.txt" "WORST_PATH_DELAY")

        echo "  WNS:       ${PASS1_WNS} ns"
        echo "  TNS:       ${PASS1_TNS} ns"
        echo "  Endpoints: ${PASS1_FAILING} failing / ${PASS1_TOTAL} total"
        echo "  Worst delay: ${PASS1_WORST_DELAY} ns"

        # ── Adaptive clock: tighten clock period from Pass 1 results ──
        if [ "${CLOCK_NS_AUTO}" = "1" ] && [ -n "${PASS1_WORST_DELAY}" ] && [ "${PASS1_WORST_DELAY}" != "null" ]; then
          # Compute tight clock = worst_path_delay * 0.95 (5% violation pressure)
          CLOCK_NS_TIGHT=$(float_mul2 "${PASS1_WORST_DELAY}" 0.95)

          # Sanity: tight clock must be positive and > 0.01 ns
          if float_gt "${CLOCK_NS_TIGHT}" "0.01"; then
            echo ""
            echo "  ── Adaptive Clock ──"
            echo "  Pass 1 worst path delay: ${PASS1_WORST_DELAY} ns"
            echo "  Tight clock: ${CLOCK_NS_TIGHT} ns (95% of worst delay)"
            echo "  Relaxed clock was: ${CLOCK_NS_RELAXED} ns"

            # Update CLOCK_NS for all subsequent stages
            CLOCK_NS="${CLOCK_NS_TIGHT}"
            export CLOCK_NS

            # Regenerate SDC with tight clock
            generate_sdc "${SDC_FILE}"
            echo "  Regenerated SDC: ${SDC_FILE} (clock=${CLOCK_NS}ns)"

            # Recompute delay target for Pass 2
            DELAY_TARGET_PS=$(float_mul "${CLOCK_NS}" 1000 0.85)
            export DELAY_TARGET_PS
            echo "  New delay target: ${DELAY_TARGET_PS} ps"

            # Re-run STA on Pass 1 netlist with the tight clock so that
            # Pass 1 WNS/TNS are measured against the same constraint as
            # Pass 2.  Without this, Pass 1 WNS (+5.6 against 10ns) would
            # always "win" against Pass 2 WNS (-0.3 against 4ns).
            echo "  Re-evaluating Pass 1 against tight clock..."
            export NETLIST_FILE="${PASS1_NETLIST}"
            export PASS_NAME="pass1"

            sta -exit ./sta_analyze.tcl > ${OUT_DIR}/pass1_sta_tight.log 2>&1
            PASS1_TIGHT_STATUS=$?
            cat ${OUT_DIR}/pass1_sta_tight.log >> ${FLOW_LOG}

            if [ $PASS1_TIGHT_STATUS -eq 0 ]; then
              PASS1_WNS=$(parse_metric "${OUT_DIR}/pass1_metrics.txt" "WNS")
              PASS1_TNS=$(parse_metric "${OUT_DIR}/pass1_metrics.txt" "TNS")
              PASS1_FAILING=$(parse_metric "${OUT_DIR}/pass1_metrics.txt" "FAILING_ENDPOINTS")
              PASS1_WORST_DELAY=$(parse_metric "${OUT_DIR}/pass1_metrics.txt" "WORST_PATH_DELAY")
              echo "  Pass 1 re-evaluated: WNS=${PASS1_WNS}ns TNS=${PASS1_TNS}ns (against ${CLOCK_NS}ns)"
            else
              echo "  WARNING: Pass 1 re-STA failed (non-fatal, keeping original metrics)"
            fi
          else
            echo "  WARNING: Adaptive clock too small (${CLOCK_NS_TIGHT}ns), keeping ${CLOCK_NS}ns"
          fi
        fi
        echo ""

        # ── Check: Is Pass 1 sufficient? ──
        # After adaptive clock tightening, Pass 1 WNS has been re-measured
        # against the tight clock (above).  If it still meets timing, we're done.
        if [ "${CLOCK_NS_AUTO}" = "1" ] && [ -n "${CLOCK_NS_TIGHT:-}" ] && float_gte "${PASS1_WNS}" "0"; then
          echo "  TIMING MET after Pass 1 (even against tight clock ${CLOCK_NS}ns)"
          BEST_PASS="pass1"
          BEST_NETLIST="${PASS1_NETLIST}"
          BEST_WNS="${PASS1_WNS}"
          BEST_TNS="${PASS1_TNS}"
          BEST_FAILING="${PASS1_FAILING}"
          PASS2_RAN="false"
        elif [ "${CLOCK_NS_AUTO}" = "1" ] && [ -n "${CLOCK_NS_TIGHT:-}" ]; then
          echo "  Adaptive clock active — proceeding to Pass 2 with tight constraint"
        elif float_gte "${PASS1_WNS}" "0"; then
          echo "  TIMING MET after Pass 1"
          BEST_PASS="pass1"
          BEST_NETLIST="${PASS1_NETLIST}"
          BEST_WNS="${PASS1_WNS}"
          BEST_TNS="${PASS1_TNS}"
          BEST_FAILING="${PASS1_FAILING}"
          PASS2_RAN="false"
        elif [ "${SKIP_PASS2}" = "1" ]; then
          echo "  Timing violated, but SKIP_PASS2=1 — skipping Pass 2"
          BEST_PASS="pass1"
          BEST_NETLIST="${PASS1_NETLIST}"
          BEST_WNS="${PASS1_WNS}"
          BEST_TNS="${PASS1_TNS}"
          BEST_FAILING="${PASS1_FAILING}"
          PASS2_RAN="false"
        fi
      fi

      # ── PASS 2: Targeted Timing Recovery (if needed) ──
      if [ -z "${BEST_PASS:-}" ]; then
        echo "── PASS 2: Targeted Timing Recovery ───────────────────"
        PASS2_RAN="true"
        PASS2_START=$(date +%s)

        # ── Pass 2a: Incremental re-mapping of Pass 1 netlist ──
        yosys -m slang -c ./synth_pass2_targeted.tcl > ${OUT_DIR}/pass2_targeted_synth.log 2>&1
        PASS2A_STATUS=$?

        cat ${OUT_DIR}/pass2_targeted_synth.log >> ${FLOW_LOG}

        if [ $PASS2A_STATUS -eq 0 ]; then
          PASS2_NETLIST="${OUT_DIR}/${TOP_MODULE}_pass2.syn.v"

          if [ -f "${PASS2_NETLIST}" ]; then
            # Run STA on targeted result
            export NETLIST_FILE="${PASS2_NETLIST}"
            export PASS_NAME="pass2"

            sta -exit ./sta_analyze.tcl > ${OUT_DIR}/pass2_sta.log 2>&1
            PASS2A_STA_STATUS=$?

            cat ${OUT_DIR}/pass2_sta.log >> ${FLOW_LOG}

            if [ $PASS2A_STA_STATUS -eq 0 ]; then
              PASS2_WNS=$(parse_metric "${OUT_DIR}/pass2_metrics.txt" "WNS")
              PASS2_TNS=$(parse_metric "${OUT_DIR}/pass2_metrics.txt" "TNS")
              PASS2_FAILING=$(parse_metric "${OUT_DIR}/pass2_metrics.txt" "FAILING_ENDPOINTS")

              PASS2A_END=$(date +%s)
              echo "  Targeted:  $((PASS2A_END - PASS2_START))s"
              echo "  WNS:       ${PASS2_WNS} ns"
              echo "  TNS:       ${PASS2_TNS} ns"

              # Check if targeted Pass 2 improved timing
              if float_gt "${PASS2_WNS}" "${PASS1_WNS}"; then
                IMPROVEMENT=$(awk "BEGIN{printf \"%.4f\", ${PASS2_WNS} - ${PASS1_WNS}}")
                echo "  Targeted re-mapping improved timing by ${IMPROVEMENT} ns"
                BEST_PASS="pass2"
                BEST_NETLIST="${PASS2_NETLIST}"
                BEST_WNS="${PASS2_WNS}"
                BEST_TNS="${PASS2_TNS}"
                BEST_FAILING="${PASS2_FAILING}"
              else
                echo "  Targeted re-mapping did not improve timing"
              fi
            else
              echo "  WARNING: Pass 2a STA failed (non-fatal)"
            fi
          else
            echo "  WARNING: Pass 2a netlist not found (non-fatal)"
          fi
        else
          echo "  WARNING: Targeted Pass 2 synthesis failed (non-fatal, trying full re-synthesis)"
        fi

        # ── Pass 2b: Full re-synthesis fallback (if targeted didn't help) ──
        if [ -z "${BEST_PASS:-}" ]; then
          echo ""
          echo "  Falling back to full re-synthesis from RTL..."

          PASS2B_START=$(date +%s)

          yosys -m slang -c ./synth_pass2_full.tcl > ${OUT_DIR}/pass2_full_synth.log 2>&1
          PASS2B_STATUS=$?

          cat ${OUT_DIR}/pass2_full_synth.log >> ${FLOW_LOG}

          if [ $PASS2B_STATUS -eq 0 ]; then
            PASS2_FULL_NETLIST="${OUT_DIR}/${TOP_MODULE}_pass2_full.syn.v"

            if [ -f "${PASS2_FULL_NETLIST}" ]; then
              export NETLIST_FILE="${PASS2_FULL_NETLIST}"
              export PASS_NAME="pass2_full"

              sta -exit ./sta_analyze.tcl > ${OUT_DIR}/pass2_full_sta.log 2>&1
              PASS2B_STA_STATUS=$?

              cat ${OUT_DIR}/pass2_full_sta.log >> ${FLOW_LOG}

              if [ $PASS2B_STA_STATUS -eq 0 ]; then
                PASS2F_WNS=$(parse_metric "${OUT_DIR}/pass2_full_metrics.txt" "WNS")
                PASS2F_TNS=$(parse_metric "${OUT_DIR}/pass2_full_metrics.txt" "TNS")
                PASS2F_FAILING=$(parse_metric "${OUT_DIR}/pass2_full_metrics.txt" "FAILING_ENDPOINTS")

                PASS2B_END=$(date +%s)
                echo "  Full:      $((PASS2B_END - PASS2B_START))s"
                echo "  WNS:       ${PASS2F_WNS} ns"
                echo "  TNS:       ${PASS2F_TNS} ns"

                # Pick best of all results
                if float_gt "${PASS2F_WNS}" "${PASS1_WNS}"; then
                  # Also compare with targeted pass2 if it ran
                  if [ -n "${PASS2_WNS:-}" ] && float_gt "${PASS2_WNS}" "${PASS2F_WNS}"; then
                    BEST_PASS="pass2"
                    BEST_NETLIST="${PASS2_NETLIST}"
                    BEST_WNS="${PASS2_WNS}"
                    BEST_TNS="${PASS2_TNS}"
                    BEST_FAILING="${PASS2_FAILING}"
                  else
                    BEST_PASS="pass2_full"
                    BEST_NETLIST="${PASS2_FULL_NETLIST}"
                    BEST_WNS="${PASS2F_WNS}"
                    BEST_TNS="${PASS2F_TNS}"
                    BEST_FAILING="${PASS2F_FAILING}"
                  fi
                else
                  echo "  Full re-synthesis did not improve over Pass 1"
                fi
              else
                echo "  WARNING: Pass 2b STA failed (non-fatal)"
              fi
            else
              echo "  WARNING: Pass 2b netlist not found (non-fatal)"
            fi
          else
            echo "  WARNING: Full re-synthesis also failed (non-fatal)"
          fi
        fi

        # If nothing improved, keep Pass 1
        if [ -z "${BEST_PASS:-}" ]; then
          BEST_PASS="pass1"
          BEST_NETLIST="${PASS1_NETLIST}"
          BEST_WNS="${PASS1_WNS}"
          BEST_TNS="${PASS1_TNS}"
          BEST_FAILING="${PASS1_FAILING}"
          echo "  Using Pass 1 result (best available)"
        fi

        PASS2_END=$(date +%s)
        echo ""
      fi

      # ── Copy best netlist to canonical output path ──
      CANONICAL_NETLIST="${OUT_DIR}/${TOP_MODULE}_${FILE_SUFFIX}.syn.v"
      cp "${BEST_NETLIST}" "${CANONICAL_NETLIST}"

      YOSYS_END=$(date +%s)
      YOSYS_TIME=$((YOSYS_END - YOSYS_START))
      YOSYS_STATUS=0

      echo "========================================================"
      echo "SYNTHESIS SUMMARY"
      echo "========================================================"
      echo "  Best Pass:    ${BEST_PASS}"
      if [ "${BEST_WNS:-unknown}" != "unknown" ]; then
        echo "  Best WNS:     ${BEST_WNS} ns"
        echo "  Best TNS:     ${BEST_TNS} ns"
        echo "  Failing:      ${BEST_FAILING} endpoints"
      fi
      echo "  Netlist:      ${CANONICAL_NETLIST}"
      echo "  Total Synth:  $(format_time $YOSYS_TIME)"
      echo "========================================================"
      echo ""

      # ── Write synthesis_summary.json ──
      {
        echo "{"
        echo "  \"design\": \"${TOP_MODULE}\","
        echo "  \"synth_mode\": \"${SYNTH_MODE}\","
        echo "  \"best_pass\": \"${BEST_PASS}\","
        echo "  \"best_netlist\": \"${CANONICAL_NETLIST}\","
        if [ "${BEST_WNS:-unknown}" != "unknown" ]; then
          echo "  \"best_wns_ns\": ${BEST_WNS},"
          echo "  \"best_tns_ns\": ${BEST_TNS},"
          echo "  \"best_failing_endpoints\": ${BEST_FAILING},"
        fi
        echo "  \"pass2_ran\": ${PASS2_RAN:-false},"
        echo "  \"clock_ns\": ${CLOCK_NS},"
        echo "  \"clock_ns_mode\": \"${CLOCK_NS_MODE}\","
        if [ "${CLOCK_NS_MODE}" = "adaptive" ]; then
          echo "  \"clock_ns_relaxed\": ${CLOCK_NS_RELAXED:-null},"
        fi
        echo "  \"delay_target_ps\": ${DELAY_TARGET_PS},"
        echo "  \"driving_cell\": \"${DRIVING_CELL}\","
        echo "  \"synth_time_seconds\": ${YOSYS_TIME}"
        echo "}"
      } > "${OUT_DIR}/synthesis_summary.json"
    fi
  fi
fi

# =============================================================================
# POST-SYNTHESIS STAGES (common to both modes)
# =============================================================================

# Check if Yosys succeeded and output exists
if [ $YOSYS_STATUS -eq 0 ] && [ -f "${OUT_DIR}/${TOP_MODULE}_${FILE_SUFFIX}.syn.v" ]; then

  # ── Check STOP_STAGE: skip PnR if synth-only ──
  if [ "${STOP_STAGE}" = "synth" ]; then
    echo "STOP_STAGE=synth — skipping OpenROAD and OpenSTA."
    OPENROAD_STATUS=0
    OPENROAD_TIME=0
    OPENSTA_STATUS=0
    OPENSTA_TIME=0
    PYTHON_STATUS=0
    PYTHON_TIME=0
  else

    # ── OpenROAD PnR Flow (with adaptive utilization step-down) ──
    # If placement fails due to over-utilization, retry with progressively
    # lower targets: user-specified (default 65%) → 50% → 35%.
    # Mirrors the adaptive retry strategy from hdlgen pnr_runner.py.
    if [ "${RUN_OPENROAD}" = "1" ]; then
      OPENROAD_START=$(date +%s)

      # Placement error patterns that indicate over-utilization
      _is_placement_failure() {
        local logfile="$1"
        local status="$2"
        [ "$status" -ne 0 ] || return 1
        # Check for known placement failure patterns in the log.
        # Case-insensitive (-i) to catch "Detailed placement failed" and
        # "detailed_placement failed" variants.
        grep -qiE "GPL-030[0-9]|DPL-003[0-9]|global.placement.fail|detailed.placement.fail|Cannot fit|PPL-0024" "$logfile" 2>/dev/null
      }

      # Build utilization step-down sequence: start with user's target, then 50, 35
      _UTIL_ORIG="${CORE_UTILIZATION:-65}"
      _UTIL_STEPS="${_UTIL_ORIG}"
      # Only add lower steps if they're actually lower than the original
      for _step in 50 35; do
        if [ "$_step" -lt "${_UTIL_ORIG}" ]; then
          _UTIL_STEPS="${_UTIL_STEPS} ${_step}"
        fi
      done

      OPENROAD_STATUS=1
      _UTIL_ATTEMPT=0
      _UTIL_FINAL="${_UTIL_ORIG}"

      for _util in ${_UTIL_STEPS}; do
        _UTIL_ATTEMPT=$((_UTIL_ATTEMPT + 1))
        _UTIL_FINAL="${_util}"
        export CORE_UTILIZATION="${_util}"

        if [ ${_UTIL_ATTEMPT} -eq 1 ]; then
          echo "Starting OpenROAD PnR flow (stop_stage=${STOP_STAGE}, platform=${PLATFORM}, util=${_util}%)..."
        else
          echo ""
          echo "  ── Placement Retry (attempt ${_UTIL_ATTEMPT}, util=${_util}%) ──"
          # Truncate the flow log section for this attempt so congestion
          # parsing only sees the latest run's markers.
          echo "--- RETRY ${_UTIL_ATTEMPT} util=${_util}% ---" >> ${FLOW_LOG}
        fi

        openroad -exit ./pnr_flow.tcl >> ${FLOW_LOG} 2>&1
        OPENROAD_STATUS=$?

        if [ $OPENROAD_STATUS -eq 0 ]; then
          break
        fi

        # Check if this is a placement failure (worth retrying) or some other error
        if ! _is_placement_failure "${FLOW_LOG}" "${OPENROAD_STATUS}"; then
          echo "OpenROAD failed with non-placement error (exit code: $OPENROAD_STATUS) — not retrying."
          break
        fi

        echo "  Placement failed at util=${_util}% (exit code: $OPENROAD_STATUS)"
      done

      OPENROAD_END=$(date +%s)
      OPENROAD_TIME=$((OPENROAD_END - OPENROAD_START))

      if [ $OPENROAD_STATUS -eq 0 ]; then
        if [ ${_UTIL_ATTEMPT} -gt 1 ]; then
          echo "OpenROAD PnR flow succeeded after ${_UTIL_ATTEMPT} attempts (final util=${_UTIL_FINAL}%, walltime: $(format_time $OPENROAD_TIME))"
        else
          echo "OpenROAD PnR flow completed successfully (walltime: $(format_time $OPENROAD_TIME))"
        fi

        # ── Extract congestion metrics from flow log ──
        # pnr_flow.tcl brackets each global_route probe with stage-tagged markers:
        #   >>> REPORT_CONGESTION_START <stage> ... <<< REPORT_CONGESTION_END <stage>
        # We parse each stage's GRT-0096 table and inject into its metrics files.
        inject_congestion "post_place_opt" "${FLOW_LOG}" "${OUT_DIR}"
        inject_congestion "post_cts_opt"   "${FLOW_LOG}" "${OUT_DIR}"
        inject_congestion "post_route_opt" "${FLOW_LOG}" "${OUT_DIR}"

      else
        echo "OpenROAD PnR flow FAILED after ${_UTIL_ATTEMPT} attempts (final util=${_UTIL_FINAL}%, walltime: $(format_time $OPENROAD_TIME))"
      fi
    else
      echo "Skipping OpenROAD PnR (RUN_OPENROAD=0)"
      OPENROAD_STATUS=0
      OPENROAD_TIME=0
    fi

    # ── OpenSTA Final Timing Analysis (backward compat) ──
    # The pnr_flow.tcl already extracts timing inside OpenROAD, but we keep
    # sta.tcl running for backward compat with process_timing_metrics.py.
    if [ $OPENROAD_STATUS -eq 0 ]; then
      echo "Starting OpenSTA timing analysis..."
      OPENSTA_START=$(date +%s)

      # Set environment variables needed by sta.tcl (final post-PnR STA)
      export NETLIST_FILE=${OUT_DIR}/${TOP_MODULE}_${FILE_SUFFIX}.syn.v

      sta -exit ./sta.tcl >> ${FLOW_LOG} 2>&1
      OPENSTA_STATUS=$?
      OPENSTA_END=$(date +%s)
      OPENSTA_TIME=$((OPENSTA_END - OPENSTA_START))

      if [ $OPENSTA_STATUS -eq 0 ]; then
        echo "OpenSTA timing analysis completed successfully (walltime: $(format_time $OPENSTA_TIME))"

        # Check if timing report was generated
        export TIMING_REPORT_FILE=${OUT_DIR}/timing_report_${FILE_SUFFIX}.txt
        if [ -f ${TIMING_REPORT_FILE} ]; then
          echo "Running Python timing analysis..."
          PYTHON_START=$(date +%s)

          # Run Python analysis script
          export OUTPUT_CSV=${OUT_DIR}/timing_metrics_${FILE_SUFFIX}.csv

          python3 ./process_timing_metrics.py >> ${FLOW_LOG} 2>&1
          PYTHON_STATUS=$?
          PYTHON_END=$(date +%s)
          PYTHON_TIME=$((PYTHON_END - PYTHON_START))

          if [ $PYTHON_STATUS -eq 0 ]; then
            echo "Python timing analysis completed successfully (walltime: $(format_time $PYTHON_TIME))"
            rm -f timing_report.txt
          else
            echo "Python timing analysis FAILED with exit code: $PYTHON_STATUS (walltime: $(format_time $PYTHON_TIME))"
          fi
        else
          echo "ERROR: Timing report file not found at: ${TIMING_REPORT_FILE}"
          OPENSTA_STATUS=1
        fi
      else
        echo "OpenSTA timing analysis FAILED with exit code: $OPENSTA_STATUS (walltime: $(format_time $OPENSTA_TIME))"
      fi
    fi

  fi  # end STOP_STAGE != synth

elif [ $YOSYS_STATUS -ne 0 ]; then
  echo "Yosys synthesis FAILED with exit code: $YOSYS_STATUS (walltime: $(format_time $YOSYS_TIME))"
else
  echo "ERROR: Yosys output file not found at: ${OUT_DIR}/${TOP_MODULE}_${FILE_SUFFIX}.syn.v"
  YOSYS_STATUS=1
fi

# =============================================================================
# PPA SUMMARY GENERATION
# =============================================================================
# Aggregate all milestone metrics into a single ppa_summary.json

if [ $YOSYS_STATUS -eq 0 ]; then
  PPA_SUMMARY="${OUT_DIR}/ppa_summary.json"
  TOTAL_TIME=$((YOSYS_TIME + OPENROAD_TIME + OPENSTA_TIME + PYTHON_TIME))

  {
    echo "{"
    echo "  \"design\": \"${TOP_MODULE}\","
    echo "  \"platform\": \"${PLATFORM}\","
    echo "  \"clock_name\": \"${CLOCK_NAME}\","
    echo "  \"clock_ns\": ${CLOCK_NS},"
    echo "  \"clock_ns_mode\": \"${CLOCK_NS_MODE}\","
    if [ "${CLOCK_NS_MODE}" = "adaptive" ]; then
      echo "  \"clock_ns_relaxed\": ${CLOCK_NS_RELAXED:-null},"
    fi
    echo "  \"stop_stage\": \"${STOP_STAGE}\","
    echo "  \"core_utilization_target\": ${_UTIL_ORIG:-65},"
    echo "  \"core_utilization_final\": ${_UTIL_FINAL:-${_UTIL_ORIG:-65}},"
    if [ "${_UTIL_ATTEMPT:-1}" -gt 1 ]; then
      echo "  \"placement_attempts\": ${_UTIL_ATTEMPT},"
    fi
    echo "  \"synth_mode\": \"${SYNTH_MODE}\","
    echo "  \"flow_wall_time_s\": ${TOTAL_TIME},"
    echo "  \"stages\": ["

    STAGE_COMMA=""

    # Post-synth metrics (from two-pass or basic flow)
    if [ "${SYNTH_MODE}" = "twopass" ] && [ -f "${OUT_DIR}/${BEST_PASS:-pass1}_metrics.json" ]; then
      SYNTH_METRICS=$(cat "${OUT_DIR}/${BEST_PASS:-pass1}_metrics.json")
      echo "    ${STAGE_COMMA}{"
      echo "      \"name\": \"post_synth\","
      echo "      \"wall_time_s\": ${YOSYS_TIME},"
      echo "      \"pass\": \"${BEST_PASS:-pass1}\","
      echo "      \"metrics\": ${SYNTH_METRICS}"
      echo "    }"
      STAGE_COMMA=","
    fi

    # Post-placement metrics
    if [ -f "${OUT_DIR}/metrics_post_place_opt.json" ]; then
      PLACE_METRICS=$(cat "${OUT_DIR}/metrics_post_place_opt.json")
      echo "    ${STAGE_COMMA}{"
      echo "      \"name\": \"post_place_opt\","
      echo "      \"metrics\": ${PLACE_METRICS}"
      echo "    }"
      STAGE_COMMA=","
    fi

    # Post-CTS metrics
    if [ -f "${OUT_DIR}/metrics_post_cts_opt.json" ]; then
      CTS_METRICS=$(cat "${OUT_DIR}/metrics_post_cts_opt.json")
      echo "    ${STAGE_COMMA}{"
      echo "      \"name\": \"post_cts_opt\","
      echo "      \"metrics\": ${CTS_METRICS}"
      echo "    }"
      STAGE_COMMA=","
    fi

    # Post-route metrics
    if [ -f "${OUT_DIR}/metrics_post_route_opt.json" ]; then
      ROUTE_METRICS=$(cat "${OUT_DIR}/metrics_post_route_opt.json")
      echo "    ${STAGE_COMMA}{"
      echo "      \"name\": \"post_route_opt\","
      echo "      \"metrics\": ${ROUTE_METRICS}"
      echo "    }"
    fi

    echo "  ]"
    echo "}"
  } > "${PPA_SUMMARY}"

  echo "PPA summary: ${PPA_SUMMARY}"
fi

# =============================================================================
# FLOW SUMMARY
# =============================================================================
echo ""
echo "========================================="
echo "EDA Flow Summary:"
echo "========================================="
echo "  Platform: ${PLATFORM}"
echo "  Stop:     ${STOP_STAGE}"
if [ "${CLOCK_NS_MODE}" = "adaptive" ]; then
  echo "  Clock:    ${CLOCK_NAME} @ ${CLOCK_NS}ns (adaptive, relaxed=${CLOCK_NS_RELAXED}ns)"
elif [ "${CLOCK_NS_MODE}" = "pdk_default" ]; then
  echo "  Clock:    ${CLOCK_NAME} @ ${CLOCK_NS}ns (PDK default for ${PLATFORM})"
else
  echo "  Clock:    ${CLOCK_NAME} @ ${CLOCK_NS}ns (user)"
fi

# Synthesis
if [ $YOSYS_STATUS -eq 0 ]; then
  if [ "${SYNTH_MODE}" = "twopass" ]; then
    echo "  Yosys:    SUCCESS [${SYNTH_MODE}, best=${BEST_PASS:-?}] (walltime: $(format_time $YOSYS_TIME))"
  else
    echo "  Yosys:    SUCCESS [basic] (walltime: $(format_time $YOSYS_TIME))"
  fi
else
  echo "  Yosys:    FAILED (exit code: $YOSYS_STATUS, walltime: $(format_time $YOSYS_TIME))"
fi

# OpenROAD
if [ $YOSYS_STATUS -eq 0 ] && [ "${STOP_STAGE}" != "synth" ]; then
  if [ "${RUN_OPENROAD}" = "1" ]; then
    if [ $OPENROAD_STATUS -eq 0 ]; then
      if [ "${_UTIL_ATTEMPT:-1}" -gt 1 ]; then
        echo "  OpenROAD: SUCCESS [→${STOP_STAGE}, util=${_UTIL_FINAL}% after ${_UTIL_ATTEMPT} attempts] (walltime: $(format_time $OPENROAD_TIME))"
      else
        echo "  OpenROAD: SUCCESS [→${STOP_STAGE}] (walltime: $(format_time $OPENROAD_TIME))"
      fi
    else
      echo "  OpenROAD: FAILED (exit code: $OPENROAD_STATUS, util=${_UTIL_FINAL:-?}%, walltime: $(format_time $OPENROAD_TIME))"
    fi
  else
    echo "  OpenROAD: SKIPPED (RUN_OPENROAD=0)"
  fi
elif [ "${STOP_STAGE}" = "synth" ]; then
  echo "  OpenROAD: SKIPPED (STOP_STAGE=synth)"
else
  echo "  OpenROAD: SKIPPED (Yosys failed)"
fi

# OpenSTA
if [ $YOSYS_STATUS -eq 0 ] && [ $OPENROAD_STATUS -eq 0 ] && [ "${STOP_STAGE}" != "synth" ]; then
  if [ $OPENSTA_STATUS -eq 0 ]; then
    echo "  OpenSTA:  SUCCESS (walltime: $(format_time $OPENSTA_TIME))"
  else
    echo "  OpenSTA:  FAILED (exit code: $OPENSTA_STATUS, walltime: $(format_time $OPENSTA_TIME))"
  fi

  if [ $OPENSTA_STATUS -eq 0 ]; then
    if [ $PYTHON_STATUS -eq 0 ]; then
      echo "  Analysis: SUCCESS (walltime: $(format_time $PYTHON_TIME))"
    else
      echo "  Analysis: FAILED (exit code: $PYTHON_STATUS, walltime: $(format_time $PYTHON_TIME))"
    fi
  else
    echo "  Analysis: SKIPPED (OpenSTA failed)"
  fi
elif [ "${STOP_STAGE}" = "synth" ]; then
  echo "  OpenSTA:  SKIPPED (STOP_STAGE=synth)"
  echo "  Analysis: SKIPPED (STOP_STAGE=synth)"
else
  echo "  OpenSTA:  SKIPPED (upstream failed)"
  echo "  Analysis: SKIPPED (upstream failed)"
fi
echo "========================================="

# Calculate and display total time
TOTAL_TIME=$((YOSYS_TIME + OPENROAD_TIME + OPENSTA_TIME + PYTHON_TIME))
if [ $TOTAL_TIME -gt 0 ]; then
  echo "  Total:    $(format_time $TOTAL_TIME)"
  echo "========================================="
fi

# PPA metric highlights
for stage_file in "${OUT_DIR}"/metrics_post_*.txt; do
  if [ -f "$stage_file" ]; then
    stage=$(grep "^STAGE=" "$stage_file" | cut -d= -f2)
    wns=$(grep "^WNS_SETUP=" "$stage_file" | cut -d= -f2)
    area=$(grep "^DESIGN_AREA=" "$stage_file" | cut -d= -f2)
    power=$(grep "^TOTAL_W=" "$stage_file" | cut -d= -f2)
    cong_ov=$(grep "^CONGESTION_OVERFLOW=" "$stage_file" | cut -d= -f2)
    cong_wl=$(grep "^CONGESTION_WIRELENGTH_UM=" "$stage_file" | cut -d= -f2)
    drc=$(grep "^DRC_COUNT=" "$stage_file" | cut -d= -f2)
    line="  ${stage}: WNS=${wns}ns Area=${area}um2 Power=${power}W"
    if [ "${cong_ov}" != "null" ] && [ -n "${cong_ov}" ]; then
      line="${line} Overflow=${cong_ov} WL=${cong_wl}um"
    fi
    if [ "${drc}" != "null" ] && [ -n "${drc}" ]; then
      line="${line} DRC=${drc}"
    fi
    echo "${line}"
  fi
done
echo "========================================="

echo ""
echo "Log file: ${FLOW_LOG}"
if [ -f "${PPA_SUMMARY:-/dev/null}" ]; then
  echo "PPA summary: ${PPA_SUMMARY}"
fi

# Exit with failure if any tool failed
if [ $YOSYS_STATUS -ne 0 ] || [ $OPENROAD_STATUS -ne 0 ] || [ $OPENSTA_STATUS -ne 0 ] || [ $PYTHON_STATUS -ne 0 ]; then
  exit 1
else
  exit 0
fi
