"""
GrowClust main driver program (converted from growclust.f90).

Implements the hybrid hierarchical clustering algorithm for relative
relocation of earthquake hypocenters based on waveform cross-correlation data.
"""

import sys
import time

import numpy as np

from . import params
from .io import (
    get_inp,
    input_check,
    param_check,
    read_evfile,
    read_stlist,
    read_xcordata,
)
from .relocation import difclust, difloc, fix_origin_time, get_tt_fast
from .stats import (
    get_samplevec,
    getmad,
    indexx,
    mean,
    mean_stddev,
    resample_data,
)
from .velocity import deptable, vzfillin


def run_growclust(infile_ctl):
    """Run the GrowClust algorithm.

    Parameters
    ----------
    infile_ctl : str
        Path to the input control file.
    """
    t0 = time.time()

    print(f"\nReading input control file: {infile_ctl}")
    print("--------------------------------------------------")

    with open(infile_ctl, "r") as f:
        # Event list
        evlist_fmt = int(get_inp(f))
        infile_evlist = get_inp(f).strip()
        print(f"Input event file format={evlist_fmt}, name={infile_evlist}")

        # Station list
        stlist_fmt = int(get_inp(f))
        infile_stlist = get_inp(f).strip()
        print(f"Input station file format={stlist_fmt}, name={infile_stlist}")

        # Xcor data
        line = get_inp(f)
        parts = line.split()
        xcordat_fmt = int(parts[0])
        tdif_fmt = int(parts[1])
        infile_xcordat = get_inp(f).strip()
        print(f"Xcor data format={xcordat_fmt}, name={infile_xcordat}")

        if tdif_fmt not in (12, 21):
            raise ValueError(f"Unknown tdif sign convention: {tdif_fmt}")
        print(f"tdif sign convention: {tdif_fmt}")

        # Velocity model files
        infile_vzmdl = get_inp(f).strip()
        vzfinefile = get_inp(f).strip()
        TTtabfile_P = get_inp(f).strip()
        TTtabfile_S = get_inp(f).strip()
        print(f"Velocity model: {infile_vzmdl}")
        print(f"TT tables: {TTtabfile_P}, {TTtabfile_S}")

        # Velocity model specs
        line = get_inp(f)
        parts = line.split()
        vpvs_factor = float(parts[0])
        rayparam_min = float(parts[1])
        if vpvs_factor < 0.01:
            vpvs_factor = np.sqrt(3.0)
        if rayparam_min < 0:
            plongcutP = 0.133
            plongcutS = 0.238
        else:
            plongcutP = rayparam_min
            plongcutS = rayparam_min * vpvs_factor
        print(f"Vp/Vs={vpvs_factor:.4f}, plongcutP={plongcutP:.4f}, plongcutS={plongcutS:.4f}")

        # TT table params
        line = get_inp(f)
        parts = line.split()
        tt_dep0, tt_dep1, tt_ddep = float(parts[0]), float(parts[1]), float(parts[2])
        line = get_inp(f)
        parts = line.split()
        tt_del0, tt_del1, tt_ddel = float(parts[0]), float(parts[1]), float(parts[2])
        print(f"TT depths: {tt_dep0}, {tt_dep1}, {tt_ddep}")
        print(f"TT distances: {tt_del0}, {tt_del1}, {tt_ddel}")
        vz_dz = tt_ddep

        # GrowClust params
        line = get_inp(f)
        parts = line.split()
        rmin, delmax, rmsmax = float(parts[0]), float(parts[1]), float(parts[2])
        print(f"rmin={rmin}, delmax={delmax}, rmsmax={rmsmax}")

        line = get_inp(f)
        parts = line.split()
        rpsavgmin = float(parts[0])
        rmincut = float(parts[1])
        ngoodmin = int(parts[2])
        iponly = int(parts[3])
        print(f"rpsavgmin={rpsavgmin}, rmincut={rmincut}, ngoodmin={ngoodmin}, iponly={iponly}")

        line = get_inp(f)
        parts = line.split()
        nboot = int(parts[0])
        nbranch_min = int(parts[1])
        print(f"nboot={nboot}, nbranch_min={nbranch_min}")

        # Output files
        outfile_cat = get_inp(f).strip()
        outfile_clust = get_inp(f).strip()
        outfile_log = get_inp(f).strip()
        outfile_boot = get_inp(f).strip()
        print(f"Output catalog: {outfile_cat}")

    print("Input control file read.")

    # Validate inputs
    input_check(plongcutP, plongcutS, vpvs_factor, tt_dep0, tt_dep1, tt_ddep,
                tt_del0, tt_del1, tt_ddel, rmsmax, delmax, iponly, nboot,
                params.MAXBOOT, params.NX0_TT, params.ND0_TT)
    param_check(params.HSHIFTMAX, params.VSHIFTMAX, params.RMEDMAX,
                params.BOXWID, params.NIT, params.SAMP_TYPE,
                params.IRELONORM, params.VZMODEL_TYPE)

    print("--------------------------------------------------\n")

    # Read event list
    print("Reading event list...")
    ev = read_evfile(evlist_fmt, infile_evlist, params.MAXEVID)
    nq = ev["nq"]
    idcusp = ev["idcusp"]
    qid2qnum = ev["qid2qnum"]
    qlat_cat = ev["qlat_cat"].copy()
    qlon_cat = ev["qlon_cat"].copy()
    qdep_cat = ev["qdep_cat"].copy()
    qyr_cat = ev["qyr_cat"]
    qmon_cat = ev["qmon_cat"]
    qdy_cat = ev["qdy_cat"]
    qhr_cat = ev["qhr_cat"]
    qmn_cat = ev["qmn_cat"]
    qsc_cat = ev["qsc_cat"].copy()
    qmag_cat = ev["qmag_cat"]
    min_qdep = ev["min_qdep"]
    max_qdep = ev["max_qdep"]
    max_qid = ev["max_qid"]

    # Initialize relocated positions
    qtim_cat = np.zeros(nq, dtype=np.float32)
    qlat = qlat_cat.copy()
    qlon = qlon_cat.copy()
    qdep = qdep_cat.astype(np.float64).copy()
    qtim = qtim_cat.copy()

    # Initialize cluster tree arrays
    index = np.arange(nq, dtype=np.int32)  # cluster number = event number
    nbranch = np.ones(nq, dtype=np.int32)
    tlat = qlat.copy()
    tlon = qlon.copy()
    tdep = qdep.copy()
    torg = qtim.astype(np.float64).copy()
    ntree = nq

    # Read station list
    print("\nReading station list...")
    slnames, sllats, sllons, slelevs, nsta, slkeys = read_stlist(infile_stlist, stlist_fmt)

    # Datum adjustment
    datum = 0.0
    if stlist_fmt == 2:
        datum = float(np.mean(slelevs[:nsta])) / 1000.0  # m -> km
        qdep += datum
        qdep_cat += datum
        tdep += datum
        min_qdep += datum
        print(f"New datum: {datum:.3f}")
        print(f"New minimum source depth: {min_qdep:.3f}")

    # Read xcor data
    print("\nReading xcor data...")
    xcor = read_xcordata(xcordat_fmt, tdif_fmt, infile_xcordat, nq, max_qid,
                         qid2qnum, qlat, qlon, rmincut, rmin, delmax, rpsavgmin,
                         iponly, ngoodmin, nsta, slnames, sllats, sllons, slelevs, slkeys)

    npair = xcor["npair"]
    nk = xcor["nk"]
    iqq1 = xcor["iqq1"]
    iqq2 = xcor["iqq2"]
    idcusp11 = xcor["idcusp11"]
    idcusp22 = xcor["idcusp22"]
    index1 = xcor["index1"]
    index2_xcor = xcor["index2"]
    ipp = xcor["ipp"]
    tdif = xcor["tdif"]
    rxcor = xcor["rxcor"]
    dist = xcor["dist"]
    slat = xcor["slat"]
    slon = xcor["slon"]

    # Quality checks
    for k in range(nk):
        if dist[k] > tt_del1:
            raise ValueError(f"Station distance {dist[k]} > tt_del1 {tt_del1}")
        if tdif[k] > params.TDIFMAX:
            raise ValueError(f"Differential time {tdif[k]} > tdifmax {params.TDIFMAX}")

    # Save copies for bootstrap
    iqq100 = iqq1.copy()
    iqq200 = iqq2.copy()
    idcusp1100 = idcusp11.copy()
    idcusp2200 = idcusp22.copy()
    index100 = index1.copy()
    index200 = index2_xcor.copy()
    ipp00 = ipp.copy()
    tdif00 = tdif.copy()
    rxcor00 = rxcor.copy()
    dist00 = dist.copy()
    slat00 = slat.copy()
    slon00 = slon.copy()

    # Make travel time tables
    print("\nMaking travel-time tables...")
    vzmin, vzmax = vzfillin(infile_vzmdl, vzfinefile, vpvs_factor, vz_dz, tt_dep0, tt_dep1)

    print(f"Creating P-phase table: {TTtabfile_P}")
    outTT_P, outX, outZ = deptable(vzfinefile, 1, plongcutP, tt_dep0, tt_dep1, tt_ddep,
                                    tt_del0, tt_del1, tt_ddel, params.VZMODEL_TYPE, TTtabfile_P)
    ndel = len(outX)
    ndep = len(outZ)
    print(f"ndel={ndel}, ndep={ndep}")

    print(f"Creating S-phase table: {TTtabfile_S}")
    outTT_S, _, _ = deptable(vzfinefile, 2, plongcutS, tt_dep0, tt_dep1, tt_ddep,
                             tt_del0, tt_del1, tt_ddel, params.VZMODEL_TYPE, TTtabfile_S)

    # Build combined travel time table: (nx, nd, 2)
    ttab = np.zeros((ndel, ndep, 2))
    ttab[:, :, 0] = outTT_P
    ttab[:, :, 1] = outTT_S
    deltab = outX
    deptab = outZ

    # Depth checks
    print(f"min, max event depth: {min_qdep:.2f}, {max_qdep:.2f}")
    print(f"min, max table depth: {tt_dep0:.2f}, {tt_dep1:.2f}")
    if max_qdep > tt_dep1:
        raise ValueError("Max event depth > max table depth")

    # Compute event pair quality factors
    print("\nComputing similarity factors...")
    ngood = np.zeros(npair, dtype=np.int32)
    rfactor = np.full(npair, -1.0)

    for ip in range(npair):
        ix1 = index1[ip]
        ix2 = index2_xcor[ip]
        if ix2 < ix1 or ix1 < 0:
            continue

        rgoodavg = 0.0
        for i in range(ix1, ix2 + 1):
            if dist[i] <= delmax and rxcor[i] >= rmin:
                ngood[ip] += 1
                rgoodavg += rxcor[i]

        if ngood[ip] > 0:
            rgoodavg /= ngood[ip]

        rfactor[ip] = ngood[ip] * rgoodavg

    # Sort by rfactor
    indxr = indexx(rfactor)
    print(f"Best pair: rfactor={rfactor[indxr[-1]]:.2f}")

    # Open log file
    log_f = open(outfile_log, "w")
    log_f.write("************************ Input files ************************\n")
    log_f.write(f"     control file:   {infile_ctl}\n")
    log_f.write(f"       event list:   {infile_evlist}\n")
    log_f.write(f"     station list:   {infile_stlist}\n")
    log_f.write(f"     xcordat file:   {infile_xcordat}\n")
    log_f.write(f"     velocity mdl:   {infile_vzmdl}\n")
    log_f.write("******************** Travel Time Tables *********************\n")
    log_f.write(f"          P-phase:   {TTtabfile_P}\n")
    log_f.write(f"          S-phase:   {TTtabfile_S}\n")
    log_f.write("*********************** Output files ************************\n")
    log_f.write(f"     catalog file:   {outfile_cat}\n")
    log_f.write(f"     cluster file:   {outfile_clust}\n")
    log_f.write(f"         log file:   {outfile_log}\n")
    log_f.write(f"   bootstrap file:   {outfile_boot}\n")
    log_f.write("**************** GROWCLUST Run Parameters *******************\n")
    log_f.write(f" rmin = {rmin:.2f}\n")
    log_f.write(f" delmax = {delmax:.1f}\n")
    log_f.write(f" rmsmax = {rmsmax:.2f}\n")
    log_f.write(f" nboot = {nboot}\n")
    log_f.write("*************************************************************\n\n")
    log_f.write("*************************************   Cluster GrowLog   ****************************************\n\n")

    # Variables for storing results
    qlatR = np.zeros(nq, dtype=np.float64)
    qlonR = np.zeros(nq, dtype=np.float64)
    qdepR = np.zeros(nq, dtype=np.float64)
    qtimR = np.zeros(nq, dtype=np.float32)
    itreeqR = np.zeros(nq, dtype=np.int32)
    nbranchqR = np.zeros(nq, dtype=np.int32)
    qcxR = np.zeros(nq, dtype=np.float64)
    qcyR = np.zeros(nq, dtype=np.float64)
    qczR = np.zeros(nq, dtype=np.float64)

    # Bootstrap storage
    if nboot > 0:
        Blon = np.zeros((nboot, nq), dtype=np.float64)
        Blat = np.zeros((nboot, nq), dtype=np.float64)
        Bdep = np.zeros((nboot, nq), dtype=np.float64)
        Btim = np.zeros((nboot, nq), dtype=np.float64)
        Bnb = np.zeros((nboot, nq), dtype=np.int32)
        Bitreeq = np.zeros((nboot, nq), dtype=np.int32)

    rng = np.random.default_rng(params.ISEED if params.ISEED != 0 else None)

    print("\nStarting GROWCLUST algorithm")

    # Bootstrap iteration loop (0 = unresampled)
    for nitb in range(nboot + 1):

        if nitb > 0:
            # Reset for bootstrap iteration
            qlat = qlat_cat.copy()
            qlon = qlon_cat.copy()
            qdep = qdep_cat.astype(np.float64).copy()
            if stlist_fmt == 2:
                qdep += datum
            qtim = qtim_cat.copy()
            index = np.arange(nq, dtype=np.int32)
            nbranch = np.ones(nq, dtype=np.int32)
            tlat = qlat.copy()
            tlon = qlon.copy()
            tdep = qdep.copy()
            torg = qtim.astype(np.float64).copy()
            ntree = nq

            iqq1 = iqq100.copy()
            iqq2 = iqq200.copy()
            idcusp11 = idcusp1100.copy()
            idcusp22 = idcusp2200.copy()

            # Generate resampling vector
            if params.SAMP_TYPE == 1:
                index1 = index100.copy()
                index2_xcor = index200.copy()
                samp_vec = np.zeros(nk, dtype=np.int32)
                for ip_idx in range(npair):
                    ix1 = index1[ip_idx]
                    ix2 = index2_xcor[ip_idx]
                    if ix2 >= 0 and ix1 >= 0:
                        n_pair = ix2 - ix1 + 1
                        samp_vec[ix1:ix2 + 1] = get_samplevec(n_pair, rng)
            else:
                samp_vec = get_samplevec(nk, rng)

                # Recompute index1, index2
                ix1n = 0
                new_index1 = np.zeros(npair, dtype=np.int32)
                new_index2 = np.zeros(npair, dtype=np.int32)
                for ip_idx in range(npair):
                    ix1 = index100[ip_idx]
                    ix2 = index200[ip_idx]
                    kcount = 0
                    if ix2 >= 0 and ix1 >= 0:
                        kcount = int(np.sum(samp_vec[ix1:ix2 + 1]))
                    new_index1[ip_idx] = ix1n
                    if kcount == 0:
                        new_index2[ip_idx] = -1
                    else:
                        new_index2[ip_idx] = ix1n + kcount - 1
                    ix1n += kcount
                index1 = new_index1
                index2_xcor = new_index2

            # Resample data arrays
            ipp = resample_data(ipp00, samp_vec)
            dist = resample_data(dist00, samp_vec).astype(np.float32)
            tdif = resample_data(tdif00, samp_vec).astype(np.float32)
            rxcor = resample_data(rxcor00, samp_vec).astype(np.float32)
            slon = resample_data(slon00, samp_vec)
            slat = resample_data(slat00, samp_vec)

            # Recompute rfactor
            ngood = np.zeros(npair, dtype=np.int32)
            rfactor = np.full(npair, -1.0)
            for ip_idx in range(npair):
                ix1 = index1[ip_idx]
                ix2 = index2_xcor[ip_idx]
                if ix2 - ix1 <= 0 or ix1 <= 0 or ix2 <= 0:
                    continue
                rgoodavg = 0.0
                for i in range(ix1, ix2 + 1):
                    if i < len(dist) and dist[i] <= delmax and rxcor[i] >= rmin:
                        ngood[ip_idx] += 1
                        rgoodavg += rxcor[i]
                if ngood[ip_idx] > 0:
                    rgoodavg /= ngood[ip_idx]
                rfactor[ip_idx] = ngood[ip_idx] * rgoodavg

            indxr = indexx(rfactor)

        # Main relocation loop: process pairs from best to worst
        for kraw in range(npair - 1, -1, -1):
            ip_idx = indxr[kraw]
            if rfactor[ip_idx] < 0.0:
                continue

            i = iqq1[ip_idx] - 1  # 0-based
            j = iqq2[ip_idx] - 1  # 0-based

            if i < 0 or j < 0 or i == j:
                continue

            # Already in same tree?
            if index[i] == index[j]:
                continue

            # Check if clusters too far apart
            qlat0 = (qlat[i] + qlat[j]) / 2.0
            qlon0 = (qlon[i] + qlon[j]) / 2.0
            qdep0 = (qdep[i] + qdep[j]) / 2.0
            cosqlat = np.cos(qlat0 / params.DEGRAD)
            dy = (tlat[index[i]] - tlat[index[j]]) * params.DEGKM
            dx = (tlon[index[i]] - tlon[index[j]]) * params.DEGKM * cosqlat
            dz = tdep[index[i]] - tdep[index[j]]
            distance = np.sqrt(dx**2 + dy**2 + dz**2)
            if distance > params.DISTMAX:
                continue

            # Find up to 10 best pairs linking these clusters
            npr8 = 0
            tdif8, ipp8, slat8, slon8 = [], [], [], []
            qlat18, qlon18, qdep18, qtim18 = [], [], [], []
            qlat28, qlon28, qdep28, qtim28 = [], [], [], []

            for kraw8 in range(kraw, -1, -1):
                ip8 = indxr[kraw8]
                if rfactor[ip8] < 0.0:
                    continue
                i8 = iqq1[ip8] - 1
                j8 = iqq2[ip8] - 1

                matched = False
                sign = 1.0

                if index[i8] == index[i] and index[j8] == index[j]:
                    matched = True
                    sign = 1.0
                    ev1, ev2 = i8, j8
                elif index[i8] == index[j] and index[j8] == index[i]:
                    matched = True
                    sign = -1.0
                    ev1, ev2 = j8, i8

                if matched:
                    npr8 += 1
                    ix1 = index1[ip8]
                    ix2 = index2_xcor[ip8]
                    for k8 in range(ix1, ix2 + 1):
                        if k8 >= len(tdif):
                            break
                        tdif8.append(sign * tdif[k8])
                        ipp8.append(ipp[k8])
                        slat8.append(slat[k8])
                        slon8.append(slon[k8])

                        qlat18.append(qlat[ev1] - tlat[index[ev1]])
                        qlon18.append(qlon[ev1] - tlon[index[ev1]])
                        qdep18.append(qdep[ev1] - tdep[index[ev1]])
                        qtim18.append(qtim[ev1] - torg[index[ev1]])
                        qlat28.append(qlat[ev2] - tlat[index[ev2]])
                        qlon28.append(qlon[ev2] - tlon[index[ev2]])
                        qdep28.append(qdep[ev2] - tdep[index[ev2]])
                        qtim28.append(qtim[ev2] - torg[index[ev2]])

                    if npr8 >= 10:
                        break

            if npr8 == 0:
                continue

            npk8 = len(tdif8)
            tdif8 = np.array(tdif8, dtype=np.float32)
            ipp8 = np.array(ipp8, dtype=np.int32)
            slat8 = np.array(slat8, dtype=np.float64)
            slon8 = np.array(slon8, dtype=np.float64)
            qlat18 = np.array(qlat18, dtype=np.float64)
            qlon18 = np.array(qlon18, dtype=np.float64)
            qdep18 = np.array(qdep18, dtype=np.float32)
            qtim18 = np.array(qtim18, dtype=np.float32)
            qlat28 = np.array(qlat28, dtype=np.float64)
            qlon28 = np.array(qlon28, dtype=np.float64)
            qdep28 = np.array(qdep28, dtype=np.float32)
            qtim28 = np.array(qtim28, dtype=np.float32)

            # Relocate cluster pair
            qlat0 = (tlat[index[i]] + tlat[index[j]]) / 2.0
            qlon0 = (tlon[index[i]] + tlon[index[j]]) / 2.0
            qdep0 = (tdep[index[i]] + tdep[index[j]]) / 2.0

            (clat1, clon1, cdep1, clat2, clon2, cdep2,
             cdist, torgdif, resid8, rms, rmed, resol) = difclust(
                qlat0, qlon0, qdep0, npk8, tdif8, ipp8, slat8, slon8,
                qlat18, qlon18, qdep18, qtim18,
                qlat28, qlon28, qdep28, qtim28,
                ttab, deltab, deptab, ndel, ndep,
                params.BOXWID, params.NIT, params.IRELONORM)

            # Rejection tests
            if cdep1 < min(0.0, min_qdep) or cdep2 < min(0.0, min_qdep):
                continue
            if abs(torgdif) > 50.0:
                raise ValueError(f"Large origin time correction: {torgdif}")
            if rms > rmsmax or rmed > params.RMEDMAX:
                continue
            if cdist > params.DISTMAX2:
                continue

            # Cluster shift tests
            nbranch_i = nbranch[index[i]]
            nbranch_j = nbranch[index[j]]

            # Combined centroid shift
            clat_old = (tlat[index[i]] * nbranch_i + tlat[index[j]] * nbranch_j) / (nbranch_i + nbranch_j)
            clon_old = (tlon[index[i]] * nbranch_i + tlon[index[j]] * nbranch_j) / (nbranch_i + nbranch_j)
            cdep_old = (tdep[index[i]] * nbranch_i + tdep[index[j]] * nbranch_j) / (nbranch_i + nbranch_j)

            clat_new = (clat1 * nbranch_i + clat2 * nbranch_j) / (nbranch_i + nbranch_j)
            clon_new = (clon1 * nbranch_i + clon2 * nbranch_j) / (nbranch_i + nbranch_j)
            cdep_new = (cdep1 * nbranch_i + cdep2 * nbranch_j) / (nbranch_i + nbranch_j)

            reject = False

            # Test cluster i shift
            if nbranch_i >= params.NCLUSTSHIFTMIN:
                dy_shift = clat1 - tlat[index[i]] - (clat_new - clat_old)
                dx_shift = clon1 - tlon[index[i]] - (clon_new - clon_old)
                dz_shift = cdep1 - tdep[index[i]] - (cdep_new - cdep_old)
                if abs(dz_shift) > params.VSHIFTMAX:
                    reject = True
                if not reject:
                    dx_shift *= cosqlat
                    delkm = np.sqrt(dx_shift**2 + dy_shift**2) * params.DEGKM
                    if delkm > params.HSHIFTMAX:
                        reject = True

            # Test cluster j shift
            if not reject and nbranch_j >= params.NCLUSTSHIFTMIN:
                dy_shift = clat2 - tlat[index[j]] - (clat_new - clat_old)
                dx_shift = clon2 - tlon[index[j]] - (clon_new - clon_old)
                dz_shift = cdep2 - tdep[index[j]] - (cdep_new - cdep_old)
                if abs(dz_shift) > params.VSHIFTMAX:
                    reject = True
                if not reject:
                    dx_shift *= cosqlat
                    delkm = np.sqrt(dx_shift**2 + dy_shift**2) * params.DEGKM
                    if delkm > params.HSHIFTMAX:
                        reject = True

            if reject:
                continue

            # APPROVED: merge clusters
            qlat_off1 = clat1 - tlat[index[i]]
            qlon_off1 = clon1 - tlon[index[i]]
            qdep_off1 = cdep1 - tdep[index[i]]
            qtim_off1 = torg[index[i]] - torgdif / 2.0

            qlat_off2 = clat2 - tlat[index[j]]
            qlon_off2 = clon2 - tlon[index[j]]
            qdep_off2 = cdep2 - tdep[index[j]]
            qtim_off2 = torg[index[j]] + torgdif / 2.0

            ntree -= 1
            itree = index[i]
            itreeold = index[j]

            nb = nbranch_i + nbranch_j
            inbranch = []

            # Reassign event locations
            for ii in range(nq):
                kk = index[ii]
                if kk == itree:
                    qlat[ii] += qlat_off1
                    qlon[ii] += qlon_off1
                    qdep[ii] += qdep_off1
                    qtim[ii] += qtim_off1
                    index[ii] = itree
                    inbranch.append(ii)
                elif kk == itreeold:
                    qlat[ii] += qlat_off2
                    qlon[ii] += qlon_off2
                    qdep[ii] += qdep_off2
                    qtim[ii] += qtim_off2
                    index[ii] = itree
                    inbranch.append(ii)

            nbranch[itree] = nb
            nbranch[itreeold] = 0

            # Write to log (only unresampled)
            if nitb == 0:
                log_f.write(f"{itree:7d}{itreeold:7d}{nbranch_i:6d}{nbranch_j:6d}"
                            f"{itree:7d}{nb:6d}{npr8:3d}{npk8:4d}"
                            f"{clat2 - clat1:10.5f}{clon2 - clon1:10.5f}"
                            f"{cdep2 - cdep1:10.5f}{distance:10.5f}"
                            f"{rms:7.2f}{rmed:7.2f}\n")

            # Shift cluster to catalog centroid, set avg time to zero
            nb_loc = nbranch[itree]
            qlat0_sum = sum(qlat[iq] for iq in inbranch)
            qlon0_sum = sum(qlon[iq] for iq in inbranch)
            qdep0_sum = sum(qdep[iq] for iq in inbranch)
            tlat_sum = sum(qlat_cat[iq] for iq in inbranch)
            tlon_sum = sum(qlon_cat[iq] for iq in inbranch)
            tdep_sum = sum(qdep_cat[iq] + (datum if stlist_fmt == 2 else 0) for iq in inbranch)
            tavg_sum = sum(qtim[iq] for iq in inbranch)

            n = nb_loc
            qlat0 = qlat0_sum / n
            qlon0 = qlon0_sum / n
            qdep0 = qdep0_sum / n
            tavg = tavg_sum / n

            tlat[itree] = tlat_sum / n
            tlon[itree] = tlon_sum / n
            tdep[itree] = tdep_sum / n
            torg[itree] = 0.0

            qlat_off = tlat[itree] - qlat0
            qlon_off = tlon[itree] - qlon0
            qdep_off = tdep[itree] - qdep0

            for iq in inbranch:
                qlat[iq] += qlat_off
                qlon[iq] += qlon_off
                qdep[iq] += qdep_off
                qtim[iq] -= tavg

        # Compute sequential tree numbers
        nb_vals = -1.0 * nbranch[:nq].astype(np.float32)
        nb_indxr = indexx(nb_vals)

        # Save old tree locations
        tlat00 = tlat.copy()
        tlon00 = tlon.copy()
        tdep00 = tdep.copy()
        torg00 = torg.copy()

        index2_new = np.zeros(nq, dtype=np.int32)
        nbranch2 = np.zeros(nq, dtype=np.int32)
        n = 0
        for new_tree in range(nq):
            old_tree = nb_indxr[new_tree]
            if nbranch[old_tree] > 0:
                index2_new[old_tree] = new_tree
                nbranch2[new_tree] = nbranch[old_tree]
                tlat[new_tree] = tlat00[old_tree]
                tlon[new_tree] = tlon00[old_tree]
                tdep[new_tree] = tdep00[old_tree]
                torg[new_tree] = torg00[old_tree]
                n += 1

        # Compute final nbranch
        nbranch_final = np.zeros(nq, dtype=np.int32)
        for iq in range(nq):
            new_tree_num = index2_new[index[iq]]
            nbranch_final[new_tree_num] += 1
        nbranch = nbranch_final

        if nitb > 0:
            # Store bootstrap results
            for iq in range(nq):
                Blon[nitb - 1, iq] = qlon[iq]
                Blat[nitb - 1, iq] = qlat[iq]
                Bdep[nitb - 1, iq] = qdep[iq]
                Btim[nitb - 1, iq] = qtim[iq]
                itree_final = index2_new[index[iq]]
                Bnb[nitb - 1, iq] = nbranch[itree_final]
                Bitreeq[nitb - 1, iq] = itree_final
        else:
            # Save unresampled results
            ntreeR = ntree
            tlatR = tlat[:ntree].copy()
            tlonR = tlon[:ntree].copy()
            tdepR = tdep[:ntree].copy()
            torgR = torg[:ntree].copy()
            nbranchtR = nbranch[:ntree].copy()

            for iq in range(nq):
                qlonR[iq] = qlon[iq]
                qlatR[iq] = qlat[iq]
                qdepR[iq] = qdep[iq]
                qtimR[iq] = qtim[iq]
                itree_final = index2_new[index[iq]]
                itreeqR[iq] = itree_final
                nbranchqR[iq] = nbranch[itree_final]

                cosqlat = np.cos(tlat[itree_final] / params.DEGRAD)
                qcxR[iq] = (qlonR[iq] - tlon[itree_final]) * params.DEGKM * cosqlat
                qcyR[iq] = (qlatR[iq] - tlat[itree_final]) * params.DEGKM
                qczR[iq] = qdepR[iq] - tdep[itree_final]

            # Compute RMS residuals
            print("Computing travel time residuals...")
            qnpair = np.zeros(nq, dtype=np.int32)
            qndiffP = np.zeros(nq, dtype=np.int32)
            qndiffS = np.zeros(nq, dtype=np.int32)
            qrmsP = np.zeros(nq)
            qrmsS = np.zeros(nq)

            CrmsP = 0.0
            CrmsS = 0.0
            npC = 0
            nsC = 0
            npairC = 0
            resP_list = []
            resS_list = []

            for ip_idx in range(npair):
                ix1 = index100[ip_idx]
                ix2 = index200[ip_idx]
                if ix2 < 0 or ix1 < 0:
                    continue

                iq1 = iqq100[ip_idx] - 1
                iq2 = iqq200[ip_idx] - 1
                itree1 = itreeqR[iq1]
                itree2 = itreeqR[iq2]

                cosqlat = np.cos(0.5 * (qlat[iq1] + qlat[iq2]) / params.DEGRAD)
                npickgoodP = 0
                npickgoodS = 0
                prmsP = 0.0
                prmsS = 0.0

                for k in range(ix1, ix2 + 1):
                    if k >= len(dist00):
                        break
                    # Event 1
                    dy = qlat[iq1] - slat00[k]
                    dx = (qlon[iq1] - slon00[k]) * cosqlat
                    dist_km = np.sqrt(dx**2 + dy**2) * params.DEGKM
                    tsec1, _ = get_tt_fast(ttab, deltab, deptab, ndel, ndep, ipp00[k], dist_km, qdep[iq1])

                    # Event 2
                    dy = qlat[iq2] - slat00[k]
                    dx = (qlon[iq2] - slon00[k]) * cosqlat
                    dist_km = np.sqrt(dx**2 + dy**2) * params.DEGKM
                    tsec2, _ = get_tt_fast(ttab, deltab, deptab, ndel, ndep, ipp00[k], dist_km, qdep[iq2])

                    tdif_pred = tsec2 - tsec1

                    if dist00[k] <= delmax and rxcor00[k] >= rmin:
                        res = tdif00[k] - (qtim[iq2] - qtim[iq1]) - tdif_pred
                        if ipp00[k] == 1:
                            resP_list.append(res)
                            npickgoodP += 1
                            prmsP += res**2
                        else:
                            resS_list.append(res)
                            npickgoodS += 1
                            prmsS += res**2

                if itree1 == itree2 and (npickgoodP + npickgoodS) > 0:
                    qnpair[iq1] += 1
                    qnpair[iq2] += 1
                    npairC += 1

                    if npickgoodP > 0:
                        qrmsP[iq1] += prmsP
                        qrmsP[iq2] += prmsP
                        qndiffP[iq1] += npickgoodP
                        qndiffP[iq2] += npickgoodP
                        CrmsP += prmsP
                        npC += npickgoodP

                    if npickgoodS > 0:
                        qrmsS[iq1] += prmsS
                        qrmsS[iq2] += prmsS
                        qndiffS[iq1] += npickgoodS
                        qndiffS[iq2] += npickgoodS
                        CrmsS += prmsS
                        nsC += npickgoodS

            CrmsP = np.sqrt(CrmsP / npC) if npC > 0 else 0.0
            CrmsS = np.sqrt(CrmsS / nsC) if nsC > 0 else 0.0

            resPmean = float(np.mean(resP_list)) if resP_list else 0.0
            resSmean = float(np.mean(resS_list)) if resS_list else 0.0

            # Per-event RMS
            qrms = np.zeros(nq)
            for iq in range(nq):
                total_ndiff = qndiffP[iq] + qndiffS[iq]
                if total_ndiff > 0:
                    qrms[iq] = np.sqrt((qrmsP[iq] + qrmsS[iq]) / total_ndiff)
                    qrmsP[iq] = np.sqrt(qrmsP[iq] / qndiffP[iq]) if qndiffP[iq] > 0 else params.RMS_NAN
                    qrmsS[iq] = np.sqrt(qrmsS[iq] / qndiffS[iq]) if qndiffS[iq] > 0 else params.RMS_NAN
                else:
                    qrms[iq] = params.RMS_NAN
                    qrmsP[iq] = params.RMS_NAN
                    qrmsS[iq] = params.RMS_NAN

            # Log summary
            nreloc = int(np.sum(nbranchqR > 1))
            ntree2 = int(np.sum(nbranch[:ntreeR] >= 2))
            ntree5 = int(np.sum(nbranch[:ntreeR] >= 5))
            ntree10 = int(np.sum(nbranch[:ntreeR] >= 10))
            ntree20 = int(np.sum(nbranch[:ntreeR] >= 20))
            ntree50 = int(np.sum(nbranch[:ntreeR] >= 50))
            ntree100 = int(np.sum(nbranch[:ntreeR] >= 100))

            log_f.write("\n" + "*" * 100 + "\n\n")
            log_f.write("************************************  GROWCLUST Run Summary  *************************************\n\n")
            log_f.write(f"{'Number of catalog events:':>55s} {nq:10d}\n")
            log_f.write(f"{'Number of relocated events:':>55s} {nreloc:10d}\n")
            log_f.write(f"{'Number of input event pairs:':>55s} {npair:10d}\n")
            log_f.write(f"{'Number of event pairs used:':>55s} {npairC:10d}\n")
            log_f.write(f"{'Number of xcor data used (P-phase):':>55s} {npC:10d}\n")
            log_f.write(f"{'RMS differential time residual (P-phase):':>55s} {CrmsP:10.4f}\n")
            log_f.write(f"{'Mean differential time residual (P-phase):':>55s} {resPmean:10.4f}\n")
            log_f.write(f"{'Number of xcor data used (S-phase):':>55s} {nsC:10d}\n")
            log_f.write(f"{'RMS differential time residual (S-phase):':>55s} {CrmsS:10.4f}\n")
            log_f.write(f"{'Mean differential time residual (S-phase):':>55s} {resSmean:10.4f}\n\n")
            log_f.write(f"{'Clusters with >=   2 events:':>55s} {ntree2:9d}\n")
            log_f.write(f"{'Clusters with >=   5 events:':>55s} {ntree5:9d}\n")
            log_f.write(f"{'Clusters with >=  10 events:':>55s} {ntree10:9d}\n")
            log_f.write(f"{'Clusters with >=  20 events:':>55s} {ntree20:9d}\n")
            log_f.write(f"{'Clusters with >=  50 events:':>55s} {ntree50:9d}\n")
            log_f.write(f"{'Clusters with >= 100 events:':>55s} {ntree100:9d}\n")
            log_f.close()

            print("GROWCLUST relocation complete for unresampled data")

        elapsed = time.time() - t0
        print(f"{'End of bootstrap iteration ' + str(nitb) if nitb > 0 else 'Done.'} ({elapsed:.1f}s)")

    # Bootstrap statistics
    if nboot > 2:
        SEh_boot = np.full(nq, params.SE_NAN)
        SEz_boot = np.full(nq, params.SE_NAN)
        SEt_boot = np.full(nq, params.SE_NAN)
        MADh_boot = np.full(nq, params.MAD_NAN)
        MADz_boot = np.full(nq, params.MAD_NAN)
        MADt_boot = np.full(nq, params.MAD_NAN)

        for iq in range(nq):
            cosqlat = np.cos(qlatR[iq] / params.DEGRAD)

            _, SEx = mean_stddev(Blon[:nboot, iq])
            _, SEy = mean_stddev(Blat[:nboot, iq])
            SEx *= cosqlat
            SEh_boot[iq] = np.sqrt(SEx**2 + SEy**2) * params.DEGKM

            _, SEz_boot[iq] = mean_stddev(Bdep[:nboot, iq])
            _, SEt = mean_stddev(Btim[:nboot, iq])
            SEt_boot[iq] = SEt

            MADx = getmad(Blon[:nboot, iq]) * cosqlat
            MADy = getmad(Blat[:nboot, iq])
            MADh_boot[iq] = np.sqrt(MADx**2 + MADy**2) * params.DEGKM
            MADz_boot[iq] = getmad(Bdep[:nboot, iq])
            MADt_boot[iq] = getmad(Btim[:nboot, iq])

            BnbMAX = int(np.max(Bnb[:nboot, iq]))
            if (qndiffP[iq] + qndiffS[iq]) < 1 or BnbMAX < 2:
                SEh_boot[iq] = params.SE_NAN
                SEz_boot[iq] = params.SE_NAN
                SEt_boot[iq] = params.SE_NAN
                MADh_boot[iq] = params.MAD_NAN
                MADz_boot[iq] = params.MAD_NAN
                MADt_boot[iq] = params.MAD_NAN
    else:
        SEh_boot = np.full(nq, params.SE_NAN)
        SEz_boot = np.full(nq, params.SE_NAN)
        SEt_boot = np.full(nq, params.SE_NAN)
        MADh_boot = np.full(nq, params.MAD_NAN)
        MADz_boot = np.full(nq, params.MAD_NAN)
        MADt_boot = np.full(nq, params.MAD_NAN)

    # Write output catalog
    print(f"\nWriting output catalog: {outfile_cat}")
    with open(outfile_cat, "w") as f:
        for iq in range(nq):
            qsc = float(qsc_cat[iq] + qtimR[iq])
            yr, mo, dy, hr, mn = int(qyr_cat[iq]), int(qmon_cat[iq]), int(qdy_cat[iq]), int(qhr_cat[iq]), int(qmn_cat[iq])
            if qsc < 0.0 or qsc >= 60.0:
                yr, mo, dy, hr, mn, qsc = fix_origin_time(yr, mo, dy, hr, mn, qsc)

            f.write(f"{yr:4d}{mo:3d}{dy:3d}{hr:3d}{mn:3d}{qsc:7.3f}"
                    f"{int(idcusp[iq]):10d}"
                    f"{qlatR[iq]:10.5f}{qlonR[iq]:11.5f}{qdepR[iq] - datum:8.3f}"
                    f"{qmag_cat[iq]:6.2f}"
                    f"{iq + 1:8d}{itreeqR[iq]:8d}{nbranchqR[iq]:8d}"
                    f"{qnpair[iq]:6d}{qndiffP[iq]:6d}{qndiffS[iq]:6d}"
                    f"{qrmsP[iq]:6.2f}{qrmsS[iq]:6.2f}"
                    f"{MADh_boot[iq]:8.3f}{MADz_boot[iq]:8.3f}{MADt_boot[iq]:8.3f}"
                    f"  {qlat_cat[iq]:10.5f}{qlon_cat[iq]:11.5f}{qdep_cat[iq] - datum:8.3f}\n")

    # Write cluster file
    if not outfile_clust.startswith("none"):
        print(f"Writing cluster file: {outfile_clust}")
        with open(outfile_clust, "w") as f:
            for ii in range(ntreeR):
                if nbranchtR[ii] < nbranch_min:
                    continue
                f.write(f"{ii:8d}{nbranchtR[ii]:8d}{tlatR[ii]:10.5f}{tlonR[ii]:11.5f}"
                        f"{tdepR[ii] - datum:8.3f}{torgR[ii]:8.3f}\n")
                for iq in range(nq):
                    if itreeqR[iq] != ii:
                        continue
                    yr, mo, dy, hr, mn = int(qyr_cat[iq]), int(qmon_cat[iq]), int(qdy_cat[iq]), int(qhr_cat[iq]), int(qmn_cat[iq])
                    qsc = float(qsc_cat[iq] + qtimR[iq])
                    f.write(f"{ii:8d}{iq + 1:9d}{int(idcusp[iq]):10d}{qmag_cat[iq]:6.2f}"
                            f"{yr:5d}{mo:3d}{dy:3d}{hr:3d}{mn:3d}{qsc:7.3f}"
                            f"{qlatR[iq]:10.5f}{qlonR[iq]:11.5f}{qdepR[iq] - datum:8.3f}"
                            f"{qcxR[iq]:10.4f}{qcyR[iq]:10.4f}{qczR[iq]:10.4f}"
                            f"{MADh_boot[iq]:8.3f}{MADz_boot[iq]:8.3f}{MADt_boot[iq]:8.3f}"
                            f"  {qlat_cat[iq]:10.5f}{qlon_cat[iq]:11.5f}{qdep_cat[iq]:8.3f}\n")

    # Write bootstrap file
    if nboot > 2 and not outfile_boot.startswith("none"):
        print(f"Writing bootstrap file: {outfile_boot}")
        with open(outfile_boot, "w") as f:
            f.write(f"{nq:8d}{nboot:6d}\n")
            for iq in range(nq):
                BnbMEAN_val = float(np.mean(Bnb[:nboot, iq]))
                BnbMIN_val = int(np.min(Bnb[:nboot, iq]))
                BnbMAX_val = int(np.max(Bnb[:nboot, iq]))
                BlatMEAN_val = float(np.mean(Blat[:nboot, iq]))
                BlonMEAN_val = float(np.mean(Blon[:nboot, iq]))
                BdepMEAN_val = float(np.mean(Bdep[:nboot, iq]))

                f.write(f"{iq + 1:8d}{int(idcusp[iq]):10d}"
                        f"{qlatR[iq]:10.5f}{qlonR[iq]:11.5f}{qdepR[iq] - datum:8.3f}"
                        f"{itreeqR[iq]:8d}{nbranchqR[iq]:8d}"
                        f"{BnbMEAN_val:7.2f}{BnbMIN_val:8d}{BnbMAX_val:8d}"
                        f"{BlatMEAN_val:10.5f}{BlonMEAN_val:11.5f}{BdepMEAN_val - datum:8.3f}"
                        f"{MADh_boot[iq]:8.3f}{MADz_boot[iq]:8.3f}{MADt_boot[iq]:8.3f}"
                        f"{SEh_boot[iq]:8.3f}{SEz_boot[iq]:8.3f}{SEt_boot[iq]:8.3f}\n")

                for ib in range(nboot):
                    f.write(f"{Blat[ib, iq]:10.5f} {Blon[ib, iq]:11.5f} "
                            f"{Bdep[ib, iq] - datum:8.5f} {Bitreeq[ib, iq]:8d} {Bnb[ib, iq]:8d}\n")

    total_time = time.time() - t0
    print(f"\nGrowClust complete. Total time: {total_time:.1f}s")


def main():
    """Command-line entry point."""
    if len(sys.argv) < 2:
        print("Usage: python -m growclust <control_file>")
        sys.exit(1)
    run_growclust(sys.argv[1])


if __name__ == "__main__":
    main()
