/**
 * TradingView custom study integration for pandas-ta custom indicators.
 *
 * Architecture overview
 * ---------------------
 * TV's custom study API only allows registering study types via the
 * `custom_indicators_getter` widget constructor option — there is no
 * dynamic registration API (createCustomStudy does not exist on the widget
 * or chart APIs).
 *
 * Study types are registered in two tiers:
 *
 *   1. Named studies — one per custom indicator type stored in indicator_types:
 *        dxo_ind_custom_my_indicator
 *      These appear in TV's indicator search with their human-readable display
 *      names so users can discover and add them from the UI.
 *
 *   2. Generic dispatch studies — backward-compatibility fallbacks:
 *        dxo_customstudy_overlay  (is_price_study: true)
 *        dxo_customstudy_pane     (is_price_study: false)
 *      Used when a named study type is not yet registered (e.g. race between
 *      widget init and indicator_types store population).
 *
 * Each named study has a single text input `_cfg` (a config key) and MAX_PLOTS
 * line plots. The constructor dispatches to `customStudyRegistry[cfgKey]` to
 * look up the per-indicator configuration and data.
 *
 * These study type names MUST also appear in the `studies_access` whitelist
 * in ChartView.vue — TV treats unlisted studies as nonexistent.
 *
 * Registration flow
 * -----------------
 * 1. Widget constructor calls getCustomIndicatorsGetter(getTypes) which registers
 *    named studies (one per type) plus the two generic fallback studies.
 * 2a. Agent path: when a custom_ indicator appears in the indicators store,
 *     registerCustomStudy() calls chart.createStudy('dxo_ind_*', ..., {_cfg: cfgKey}).
 * 2b. Picker path: user selects a named study from TV's search; TV creates it with
 *     empty _cfg; convertTVStudyToIndicator (useTradingViewIndicators) converts
 *     the dxo_ind_* event into an IndicatorInstance and adds it to the store;
 *     registerCustomStudy() then *adopts* the existing study by setting its _cfg
 *     input (no duplicate createStudy call).
 * 3. TV calls the study's init(ctx, inputs):
 *    a. Reads symbol/period from ctx; builds the data cache key.
 *    b. Fires an async evaluateIndicator WebSocket request.
 * 4. When data arrives the constructor calls the registered refreshCallback
 *    which calls IStudyApi.setInputValues() with a new config key, causing
 *    TV to re-run init()+main() with the now-populated cache.
 *
 * IMPORTANT: call getCustomIndicatorsGetter() and pass it as the
 * `custom_indicators_getter` option when creating the TradingView widget.
 */

import { watch, ref } from 'vue'
import { useIndicatorStore, type IndicatorInstance, type CustomIndicatorMetadata } from '../stores/indicators'
import { useIndicatorTypesStore, type CustomIndicatorType } from '../stores/indicatorTypes'
import { useChartStore } from '../stores/chart'
import { wsManager, type MessageHandler } from './useWebSocket'
import { intervalToSeconds } from '../utils'

// ---------------------------------------------------------------------------
// WebSocket helper — evaluate_indicator request/response
// ---------------------------------------------------------------------------

interface EvaluateResult {
  symbol: string
  period_seconds: number
  pandas_ta_name: string
  parameters: Record<string, any>
  candle_count: number
  columns: string[]
  values: Array<Record<string, any>>
  error?: string
}

function evaluateIndicator(
  symbol: string,
  fromTime: number,
  toTime: number,
  periodSeconds: number,
  pandasTaName: string,
  parameters: Record<string, any>,
  timeoutMs = 30_000
): Promise<EvaluateResult> {
  const requestId = `cind_${Date.now()}_${Math.random().toString(36).substring(7)}`

  return new Promise((resolve, reject) => {
    const timer = window.setTimeout(() => {
      wsManager.removeHandler(handler)
      reject(new Error(`evaluate_indicator timeout for ${pandasTaName}`))
    }, timeoutMs)

    const handler: MessageHandler = (message: any) => {
      if (message.type !== 'evaluate_indicator_result') return
      if (message.request_id !== requestId) return
      clearTimeout(timer)
      wsManager.removeHandler(handler)
      if (message.error) reject(new Error(message.error))
      else resolve(message as EvaluateResult)
    }

    wsManager.addHandler(handler)
    wsManager.send({
      type: 'evaluate_indicator',
      request_id: requestId,
      symbol,
      from_time: fromTime,
      to_time: toTime,
      period_seconds: periodSeconds,
      pandas_ta_name: pandasTaName,
      parameters,
    })
  })
}

// ---------------------------------------------------------------------------
// Data cache — keyed by "indicatorId_symbol_periodSeconds_paramsHash"
// Each entry maps timestamp-ms → row object
// ---------------------------------------------------------------------------

type DataRow = Record<string, number | null>
const dataCache = new Map<string, Map<number, DataRow>>()

function cacheKey(indicatorId: string, symbol: string, periodSeconds: number, paramsHash: string): string {
  return `${indicatorId}_${symbol}_${periodSeconds}_${paramsHash}`
}

function buildDataCache(result: EvaluateResult): Map<number, DataRow> {
  const map = new Map<number, DataRow>()
  for (const point of result.values) {
    const tsMs = (point.timestamp as number) * 1000  // server sends Unix seconds → ms
    map.set(tsMs, point as DataRow)
  }
  return map
}

// ---------------------------------------------------------------------------
// Custom study registry — config map shared between getter and composable
// ---------------------------------------------------------------------------

interface CustomStudyEntry {
  indicatorId: string
  pandasTaName: string
  parameters: Record<string, any>
  metadata: CustomIndicatorMetadata
}

// cfgKey → per-instance config; populated by registerCustomStudy()
const customStudyRegistry = new Map<string, CustomStudyEntry>()

// indicatorId → callback(newCfgKey); set by registerCustomStudy()
// Called by the constructor when async data arrives to trigger TV re-run.
const refreshCallbacks = new Map<string, (newCfgKey: string) => void>()

// indicatorId → { fromTime, toTime } of the last evaluate_indicator request.
// Used to detect when the visible range has expanded beyond the fetched range.
const fetchedRanges = new Map<string, { fromTime: number; toTime: number }>()

// TradingView widget reference — set by useCustomIndicators() so the
// constructor can query the current visible range.
let _tvWidget: any = null

// Set of pandas_ta_names that were registered as named TV studies at widget init.
// Types that arrive after init are NOT in TV's registry, so must use the generic fallback.
const _registeredStudyNames = new Set<string>()

// ---------------------------------------------------------------------------
// Generic study design constants
// ---------------------------------------------------------------------------

const MAX_PLOTS = 8

const MULTI_LINE_COLORS = [
  '#2196F3', '#FF9800', '#4CAF50', '#E91E63', '#9C27B0',
  '#00BCD4', '#FF5722', '#795548',
]

// ---------------------------------------------------------------------------
// Study name helpers
// ---------------------------------------------------------------------------

/** TV study type name for a given pandas_ta_name. */
function tvStudyName(pandasTaName: string): string {
  return `dxo_ind_${pandasTaName}`
}

/** Build the constructor function. nOutputs controls how many values main() returns. */
function makeStudyConstructor(nOutputs: number = MAX_PLOTS) {
  return function (this: any) {
    let _cfgKey = ''
    let _dataKey = ''
    let _fetchGen = 0

    this.init = function (ctx: any, inputs: (i: number) => any) {
      const cfgKey = inputs(0) as string
      _cfgKey = cfgKey
      _fetchGen++
      const myGen = _fetchGen

      const entry = customStudyRegistry.get(cfgKey)
      if (!entry) return

      const symbol: string = ctx.symbol.ticker
      const periodStr: string = ctx.symbol.period
      const periodSeconds = intervalToSeconds(periodStr)
      const paramsHash = JSON.stringify(entry.parameters)
      const dk = cacheKey(entry.indicatorId, symbol, periodSeconds, paramsHash)
      _dataKey = dk

      if (dataCache.has(dk)) return

      let fromTime: number
      let toTime: number
      const now = Math.floor(Date.now() / 1000)
      toTime = now
      fromTime = now - periodSeconds * 500
      if (_tvWidget) {
        try {
          const range = _tvWidget.activeChart().getVisibleRange()
          if (range?.from && range?.to) {
            const dur = Math.floor(range.to) - Math.floor(range.from)
            fromTime = Math.floor(range.from) - Math.floor(dur * 0.5)
            toTime = Math.floor(range.to)
          }
        } catch { /* chart not yet ready */ }
      }

      const capturedDk = dk
      const capturedCfgKey = cfgKey

      // Record the fetched range so range-expansion watchers can detect gaps
      fetchedRanges.set(entry.indicatorId, { fromTime, toTime })

      evaluateIndicator(symbol, fromTime, toTime, periodSeconds, entry.pandasTaName, entry.parameters)
        .then((result) => {
          if (myGen !== _fetchGen) return
          dataCache.set(capturedDk, buildDataCache(result))
          const refreshKey = `${capturedCfgKey}__r`
          customStudyRegistry.set(refreshKey, entry)
          const cb = refreshCallbacks.get(entry.indicatorId)
          if (cb) cb(refreshKey)
        })
        .catch((err) => {
          console.error('[CustomIndicators] Failed to fetch data for', entry.pandasTaName, err)
          fetchedRanges.delete(entry.indicatorId)
        })
    }

    this.main = function (ctx: any, _inputs: (i: number) => any) {
      const ts: number = ctx.symbol.bartime()
      if (!_cfgKey || !_dataKey) return new Array(nOutputs).fill(NaN)
      const entry = customStudyRegistry.get(_cfgKey)
      if (!entry) return new Array(nOutputs).fill(NaN)
      const cache = dataCache.get(_dataKey)
      if (!cache) return new Array(nOutputs).fill(NaN)
      const row = cache.get(ts)
      return Array.from({ length: nOutputs }, (_, i) => {
        const col = entry.metadata.output_columns[i]
        return col && row ? (row[col.name] as number) ?? NaN : NaN
      })
    }
  }
}

// ---------------------------------------------------------------------------
// Study definition builders
// ---------------------------------------------------------------------------

function makeStudyMetainfo(name: string, description: string, isPriceStudy: boolean): any {
  const plots = Array.from({ length: MAX_PLOTS }, (_, i) => ({ id: `plot_${i}`, type: 'line' }))
  const styles: Record<string, any> = {}
  const defaultStyles: Record<string, any> = {}
  for (let i = 0; i < MAX_PLOTS; i++) {
    styles[`plot_${i}`] = { title: `Plot ${i}` }
    defaultStyles[`plot_${i}`] = {
      linestyle: 0,
      linewidth: 1,
      plottype: 0,
      color: MULTI_LINE_COLORS[i % MULTI_LINE_COLORS.length],
      visible: i === 0,
    }
  }

  return {
    _metainfoVersion: 53,
    id: `${name}@tv-basicstudies-1`,
    scriptIdPart: '',
    name,
    description,
    shortDescription: description,
    is_hidden_study: false,
    is_price_study: isPriceStudy,
    isCustomIndicator: true,
    format: { type: 'inherit' },
    inputs: [
      { id: '_cfg', name: 'Config Key', type: 'text', defval: '' },
    ],
    plots,
    styles,
    defaults: {
      inputs: { _cfg: '' },
      styles: defaultStyles,
    },
  }
}

function tvInputType(paramType: string): string {
  switch (paramType) {
    case 'int': return 'integer'
    case 'float': return 'float'
    case 'bool': return 'bool'
    default: return 'text'
  }
}

/** Create metainfo for a named study with real parameter inputs and exact output count. */
function makeNamedStudyMetainfo(name: string, type: CustomIndicatorType): any {
  const meta = type.metadata
  const nOutputs = meta.output_columns.length
  const isPriceStudy = meta.pane === 'price'

  const plots = Array.from({ length: nOutputs }, (_, i) => ({ id: `plot_${i}`, type: 'line' }))
  const styles: Record<string, any> = {}
  const defaultStyles: Record<string, any> = {}
  for (let i = 0; i < nOutputs; i++) {
    const col = meta.output_columns[i]
    styles[`plot_${i}`] = { title: col.display_name || col.name }
    defaultStyles[`plot_${i}`] = {
      linestyle: 0,
      linewidth: col.plot?.linewidth ?? 1,
      plottype: col.plot?.style ?? 0,
      color: col.plot?.color ?? MULTI_LINE_COLORS[i % MULTI_LINE_COLORS.length],
      visible: col.plot?.visible ?? true,
    }
  }

  // _cfg is the internal dispatch key — hidden from the user
  const inputs: any[] = [
    { id: '_cfg', name: 'Config Key', type: 'text', defval: '', isHidden: true },
  ]
  const defaultInputs: Record<string, any> = { _cfg: '' }

  for (const [paramName, param] of Object.entries(meta.parameters)) {
    const input: any = {
      id: `param_${paramName}`,
      name: paramName,
      type: tvInputType(param.type),
      defval: param.default,
    }
    if (param.min !== undefined) input.min = param.min
    if (param.max !== undefined) input.max = param.max
    if (param.description) input.tooltip = param.description
    inputs.push(input)
    defaultInputs[`param_${paramName}`] = param.default
  }

  return {
    _metainfoVersion: 53,
    id: `${name}@tv-basicstudies-1`,
    scriptIdPart: '',
    name,
    description: meta.display_name,
    shortDescription: meta.display_name,
    is_hidden_study: false,
    is_price_study: isPriceStudy,
    isCustomIndicator: true,
    format: { type: 'inherit' },
    inputs,
    plots,
    styles,
    defaults: {
      inputs: defaultInputs,
      styles: defaultStyles,
    },
  }
}

/** Create a generic fallback dispatch study (overlay or separate pane). Hidden from picker. */
function makeGenericStudy(name: string, isPriceStudy: boolean): any {
  const metainfo = makeStudyMetainfo(name, name, isPriceStudy)
  metainfo.is_hidden_study = true
  return {
    name,
    metainfo,
    constructor: makeStudyConstructor(),
  }
}

/** Create a named study for a specific custom indicator type. */
function makeNamedStudy(type: CustomIndicatorType): any {
  const name = tvStudyName(type.pandas_ta_name)
  const nOutputs = type.metadata.output_columns.length
  return {
    name,
    metainfo: makeNamedStudyMetainfo(name, type),
    constructor: makeStudyConstructor(nOutputs),
  }
}

// ---------------------------------------------------------------------------
// Custom indicators getter
// Pass the result of this function as the widget option:
//   custom_indicators_getter: getCustomIndicatorsGetter(getTypes)
//
// The study type names must also be listed in studies_access in ChartView.vue.
// ---------------------------------------------------------------------------

/** Static test study — a plain close-price line to verify custom_indicators_getter works. */
function makeTestStudy(PineJS: any): any {
  return {
    name: 'dxo_test_close',
    metainfo: {
      _metainfoVersion: 53,
      id: 'dxo_test_close@tv-custom-1',
      scriptIdPart: '',
      name: 'dxo_test_close',
      description: 'DXO Test (close price)',
      shortDescription: 'DXO Test',
      is_hidden_study: false,
      is_price_study: true,
      isCustomIndicator: true,
      linkedToSeries: true,
      format: { type: 'inherit' },
      inputs: [],
      plots: [{ id: 'plot_0', type: 'line' }],
      styles: { plot_0: { title: 'Close', joinPoints: false } },
      defaults: {
        inputs: {},
        styles: {
          plot_0: { linestyle: 0, linewidth: 2, color: '#FF0000', transparency: 0, visible: true },
        },
      },
    },
    constructor: function (this: any) {
      this.main = function (ctx: any, _inputs: any) {
        this._context = ctx
        return [PineJS.Std.close(ctx)]
      }
    },
  }
}

export function getCustomIndicatorsGetter(
  getTypes: () => Record<string, CustomIndicatorType>
): (_PineJS: any) => Promise<any[]> {
  return (PineJS: any): Promise<any[]> => {
    const types = getTypes()
    const typeKeys = Object.keys(types)
    console.log('[CustomIndicators] custom_indicators_getter called, types in store:', typeKeys)

    _registeredStudyNames.clear()
    const namedStudies = Object.values(types).flatMap(type => {
      try {
        _registeredStudyNames.add(type.pandas_ta_name)
        return [makeNamedStudy(type)]
      } catch (err) {
        console.error('[CustomIndicators] Skipping malformed indicator type:', type?.pandas_ta_name, err)
        return []
      }
    })
    const testStudy = makeTestStudy(PineJS)
    const studies = [
      makeGenericStudy('dxo_customstudy_overlay', true),
      makeGenericStudy('dxo_customstudy_pane', false),
      testStudy,
      ...namedStudies,
    ]
    console.log('[CustomIndicators] Registering studies:', studies.map((s) => s.name))
    return Promise.resolve(studies)
  }
}

// ---------------------------------------------------------------------------
// Main composable
// ---------------------------------------------------------------------------

export function useCustomIndicators(tvWidget: any) {
  _tvWidget = tvWidget

  const indicatorStore = useIndicatorStore()
  const indicatorTypesStore = useIndicatorTypesStore()
  const chartStore = useChartStore()

  // Maps indicator id → { cfgKey, tvStudyId, symbol }
  const registered = new Map<string, { cfgKey: string; tvStudyId: string | null; symbol: string }>()
  // Tracks indicator IDs whose registerCustomStudy() is in-flight (createStudy not yet resolved).
  const pendingRegistration = new Set<string>()
  // Monotonic version counter per indicator for unique config keys
  const cfgVersions = new Map<string, number>()
  // Last-seen parameter hash per indicator id for change detection.
  const lastParams = new Map<string, string>()

  let isChartReady = false

  function nextCfgKey(indicatorId: string): string {
    const v = (cfgVersions.get(indicatorId) || 0) + 1
    cfgVersions.set(indicatorId, v)
    return `cfg_${indicatorId.replace(/[^a-zA-Z0-9]/g, '_')}_v${v}`
  }

  function applyStudyOverrides(studyId: string, meta: CustomIndicatorMetadata) {
    try {
      const study = tvWidget.activeChart().getStudyById(studyId)
      if (!study) return
      const cols = meta.output_columns
      const overrides: Record<string, any> = {}

      for (let i = 0; i < MAX_PLOTS; i++) {
        const col = cols[i]
        if (col == null) {
          overrides[`styles.plot_${i}.visible`] = false
          continue
        }
        const p = col.plot
        overrides[`styles.plot_${i}.visible`] = p?.visible ?? true
        overrides[`styles.plot_${i}.plottype`] = p?.style ?? 0
        overrides[`styles.plot_${i}.linewidth`] = p?.linewidth ?? 2
        overrides[`styles.plot_${i}.linestyle`] = 0
        overrides[`styles.plot_${i}.color`] = p?.color ?? MULTI_LINE_COLORS[i % MULTI_LINE_COLORS.length]
      }

      study.applyOverrides(overrides)
    } catch (err) {
      console.warn('[CustomIndicators] Could not apply overrides:', err)
    }
  }

  function applyStudyTitle(studyId: string, meta: CustomIndicatorMetadata) {
    try {
      const study = tvWidget.activeChart().getStudyById(studyId)
      if (!study) return
      const displayName = meta.display_name
      if (typeof study.setStudyTitle === 'function') {
        study.setStudyTitle(displayName)
      }
    } catch { /* setStudyTitle not available in this TV build */ }
  }

  // ------------------------------------------------------------------
  // Resolve the study type name to use when creating a new TV study
  // ------------------------------------------------------------------
  function resolveStudyTypeName(pandasTaName: string, pane: string): string {
    // Only use the named study if it was registered in TV at widget init time.
    // Types that arrived after init are not in TV's registry — fall back to generic.
    if (_registeredStudyNames.has(pandasTaName)) {
      const typeEntry = indicatorTypesStore.types[pandasTaName]
      if (typeEntry) return typeEntry.metadata.display_name
    }
    return pane === 'price' ? 'dxo_customstudy_overlay' : 'dxo_customstudy_pane'
  }

  // ------------------------------------------------------------------
  // Register a custom indicator as a TV study instance
  // ------------------------------------------------------------------
  async function registerCustomStudy(indicator: IndicatorInstance) {
    const meta = indicator.custom_metadata
      ?? indicatorTypesStore.types[indicator.pandas_ta_name]?.metadata
    if (!meta) {
      console.warn('[CustomIndicators] No custom_metadata on indicator:', indicator.id)
      return
    }

    if (pendingRegistration.has(indicator.id)) return
    pendingRegistration.add(indicator.id)

    const symbol = indicator.symbol || chartStore.symbol
    const cfgKey = nextCfgKey(indicator.id)

    // Store per-instance config in the registry so the constructor can find it
    customStudyRegistry.set(cfgKey, {
      indicatorId: indicator.id,
      pandasTaName: indicator.pandas_ta_name,
      parameters: indicator.parameters,
      metadata: meta,
    })

    // Register the callback invoked by the constructor after async data loads.
    refreshCallbacks.set(indicator.id, (newCfgKey: string) => {
      const entry = registered.get(indicator.id)
      if (!entry?.tvStudyId) return
      try {
        const study = tvWidget.activeChart().getStudyById(entry.tvStudyId)
        if (study) {
          registered.set(indicator.id, { ...entry, cfgKey: newCfgKey })
          study.setInputValues([{ id: '_cfg', value: newCfgKey }])
        }
      } catch (err) {
        console.warn('[CustomIndicators] Could not refresh study after data load:', err)
      }
    })

    try {
      // Check if the study already exists on the chart (picker-added path):
      // when a user picks a named study from TV's indicator search, TV creates
      // it with empty _cfg and fires study_event. useTradingViewIndicators converts
      // that to an IndicatorInstance with tv_study_id set. We must adopt the
      // existing study rather than create a duplicate.
      let tvStudyId: string | null = null
      let adopted = false

      if (indicator.tv_study_id) {
        try {
          const existingStudy = tvWidget.activeChart().getStudyById(indicator.tv_study_id)
          if (existingStudy) {
            tvStudyId = indicator.tv_study_id
            adopted = true
          }
        } catch { /* study not on chart */ }
      }

      if (adopted && tvStudyId) {
        // Picker path: adopt the existing study by setting its _cfg + param inputs.
        const study = tvWidget.activeChart().getStudyById(tvStudyId)
        if (study) {
          const inputValues: Array<{ id: string; value: any }> = [{ id: '_cfg', value: cfgKey }]
          for (const [paramName, value] of Object.entries(indicator.parameters)) {
            inputValues.push({ id: `param_${paramName}`, value })
          }
          study.setInputValues(inputValues)
        }
        console.log('[CustomIndicators] Adopted picker-added study:', indicator.pandas_ta_name, '(', tvStudyId, ')')
      } else {
        // Agent path: create a new study on the chart.
        const forceOverlay = meta.pane === 'price'
        const studyTypeName = resolveStudyTypeName(indicator.pandas_ta_name, meta.pane)

        const inputsDict: Record<string, any> = { _cfg: cfgKey }
        for (const [paramName, value] of Object.entries(indicator.parameters)) {
          inputsDict[`param_${paramName}`] = value
        }

        tvStudyId = (await tvWidget.activeChart().createStudy(
          studyTypeName, forceOverlay, false,
          inputsDict
        )) as string | null

        console.log('[CustomIndicators] Created study:', indicator.pandas_ta_name, '→', studyTypeName, '(', tvStudyId, ')')
      }

      registered.set(indicator.id, { cfgKey, tvStudyId: tvStudyId ?? null, symbol })
      lastParams.set(indicator.id, JSON.stringify(indicator.parameters))

      if (tvStudyId) {
        applyStudyTitle(tvStudyId, meta)
        applyStudyOverrides(tvStudyId, meta)
        if (tvStudyId !== indicator.tv_study_id) {
          indicatorStore.updateIndicator(indicator.id, { tv_study_id: tvStudyId })
        }
      }
    } catch (err) {
      console.error('[CustomIndicators] Failed to register TV custom study:', indicator.pandas_ta_name, err)
    } finally {
      pendingRegistration.delete(indicator.id)
    }
  }

  // ------------------------------------------------------------------
  // Remove a custom study from the chart
  // ------------------------------------------------------------------
  function removeCustomStudy(indicatorId: string) {
    const entry = registered.get(indicatorId)
    if (!entry) return
    registered.delete(indicatorId)
    lastParams.delete(indicatorId)
    refreshCallbacks.delete(indicatorId)
    fetchedRanges.delete(indicatorId)

    pendingRegistration.delete(indicatorId)
    if (entry.tvStudyId) {
      try { tvWidget.activeChart().removeStudy(entry.tvStudyId) } catch { /* already gone */ }
    }
  }

  // ------------------------------------------------------------------
  // Re-register when parameters/symbol/period change (forces new data fetch)
  // ------------------------------------------------------------------
  async function refreshCustomStudy(indicator: IndicatorInstance) {
    for (const key of Array.from(dataCache.keys())) {
      if (key.startsWith(`${indicator.id}_`)) {
        dataCache.delete(key)
      }
    }
    removeCustomStudy(indicator.id)
    await registerCustomStudy(indicator)
  }

  // ------------------------------------------------------------------
  // Chart ready — set isChartReady and track when TV data has loaded
  // ------------------------------------------------------------------
  isChartReady = true

  // Reactive flag: true once TV has loaded chart data (earliest safe point to create studies).
  // Using ref() so it can be a watched source alongside indicatorStore.indicators.
  const isDataLoaded = ref(false)

  tvWidget.activeChart().onDataLoaded().subscribe(null, () => {
    if (isDataLoaded.value) return
    console.log('[CustomIndicators] onDataLoaded fired')
    isDataLoaded.value = true
  })

  // ------------------------------------------------------------------
  // Combined watcher: fires when data loads OR store changes.
  // immediate:true handles the case where indicators are already in the
  // store before this composable is called.
  // ------------------------------------------------------------------
  watch(
    [isDataLoaded, () => indicatorStore.indicators],
    async ([loaded, newIndicators]) => {
      if (!isChartReady || !loaded) return

      const indicators = newIndicators as Record<string, InstanceType<any>>

      for (const [id, indicator] of Object.entries(indicators)) {
        if (!indicator.pandas_ta_name.startsWith('custom_')) continue

        if (!registered.has(id) && !pendingRegistration.has(id)) {
          lastParams.set(id, JSON.stringify(indicator.parameters))
          await registerCustomStudy(indicator)
        } else {
          const entry = registered.get(id)!
          const currParams = JSON.stringify(indicator.parameters)
          const prevParams = lastParams.get(id)
          const currSymbol = indicator.symbol || chartStore.symbol
          if (currParams !== prevParams || currSymbol !== entry.symbol) {
            lastParams.set(id, currParams)
            await refreshCustomStudy(indicator)
          }
        }
      }

      // Handle removals
      for (const id of registered.keys()) {
        if (!(id in indicators)) {
          lastParams.delete(id)
          removeCustomStudy(id)
        }
      }
    },
    { immediate: true, deep: true }
  )

  // Re-fetch when chart resolution changes
  watch(
    () => chartStore.period,
    () => {
      if (!isChartReady) return
      for (const [id, indicator] of Object.entries(indicatorStore.indicators)) {
        if (!indicator.pandas_ta_name.startsWith('custom_')) continue
        if (registered.has(id)) {
          lastParams.set(id, JSON.stringify(indicator.parameters))
          refreshCustomStudy(indicator)
        }
      }
    }
  )

  // Re-fetch when visible range expands beyond the fetched range (e.g. user zooms out).
  // Debounced to avoid hammering on every scroll tick.
  // Due to lookback effects, we always re-fetch the full new range rather than just the gap.
  let rangeWatchTimer: ReturnType<typeof setTimeout> | null = null
  watch(
    [() => chartStore.start_time, () => chartStore.end_time],
    ([newStart, newEnd]) => {
      if (!isChartReady) return
      if (rangeWatchTimer) clearTimeout(rangeWatchTimer)
      rangeWatchTimer = setTimeout(() => {
        rangeWatchTimer = null
        for (const [id, indicator] of Object.entries(indicatorStore.indicators)) {
          if (!indicator.pandas_ta_name.startsWith('custom_')) continue
          if (!registered.has(id) || pendingRegistration.has(id)) continue
          const range = fetchedRanges.get(id)
          if (!range) continue
          const startExpanded = newStart != null && newStart < range.fromTime
          // Allow a 10-bar buffer on the right before triggering (live bars naturally advance)
          const endExpanded = newEnd != null && newEnd > range.toTime + chartStore.period * 10
          if (startExpanded || endExpanded) {
            console.log('[CustomIndicators] Visible range expanded beyond fetch range, re-fetching:', indicator.pandas_ta_name, { newStart, rangeFrom: range.fromTime })
            refreshCustomStudy(indicator)
          }
        }
      }, 500)
    }
  )

  // ------------------------------------------------------------------
  // Refresh studies when the indicator script is edited externally
  // ------------------------------------------------------------------
  const indicatorUpdatedHandler: MessageHandler = (message) => {
    if (message.type !== 'event' || message.eventType !== 'INDICATOR_UPDATED') return
    const pandasTaName: string | undefined = message.payload?.indicatorName
    if (!pandasTaName) return
    for (const indicator of Object.values(indicatorStore.indicators)) {
      if (indicator.pandas_ta_name !== pandasTaName) continue
      if (!registered.has(indicator.id)) continue
      console.log('[CustomIndicators] Indicator updated, refreshing:', pandasTaName, indicator.id)
      refreshCustomStudy(indicator)
    }
  }
  wsManager.addHandler(indicatorUpdatedHandler)

  // Cleanup
  return () => {
    wsManager.removeHandler(indicatorUpdatedHandler)
    if (rangeWatchTimer) { clearTimeout(rangeWatchTimer); rangeWatchTimer = null }
    for (const id of [...registered.keys()]) {
      removeCustomStudy(id)
    }
    registered.clear()
    pendingRegistration.clear()
    cfgVersions.clear()
    lastParams.clear()
    if (_tvWidget === tvWidget) _tvWidget = null
    isChartReady = false
  }
}
