const LOCK_TTL_MS = 12_000
const HEARTBEAT_MS = 4_000
const SESSION_PROBE_WAIT_MS = 80
export const PROJECT_LOCK_KEY_PREFIX = 'jgjs-project-lock:'
const CHANNEL_NAME = 'jgjs-project-lock-channel'
const TAB_SESSION_ID_KEY = 'jgjs-project-tab-session-id'
const runtimeInstanceId = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`

type LockPayload = {
  sessionId: string
  projectId: string
  updatedAt: number
}

const now = () => Date.now()
const lockKeyOf = (projectId: string) => `${PROJECT_LOCK_KEY_PREFIX}${projectId}`

const parseLockPayload = (raw: string | null): LockPayload | null => {
  if (!raw) return null
  try {
    const data = JSON.parse(raw) as Partial<LockPayload>
    if (!data || typeof data !== 'object') return null
    if (typeof data.sessionId !== 'string' || typeof data.projectId !== 'string') return null
    if (typeof data.updatedAt !== 'number' || !Number.isFinite(data.updatedAt)) return null
    return {
      sessionId: data.sessionId,
      projectId: data.projectId,
      updatedAt: data.updatedAt
    }
  } catch {
    return null
  }
}

const isExpired = (payload: LockPayload) => now() - payload.updatedAt > LOCK_TTL_MS

const randomSessionId = () => `${now()}-${Math.random().toString(36).slice(2, 10)}`

const debugLock = (message: string, payload?: Record<string, unknown>) => {
  try {
    if (payload) {
      console.debug('[project-lock]', message, payload)
    } else {
      console.debug('[project-lock]', message)
    }
  } catch {
    // ignore console errors
  }
}

const getOrCreateTabSessionId = () => {
  try {
    const existing = String(window.sessionStorage.getItem(TAB_SESSION_ID_KEY) || '').trim()
    if (existing) return existing
    const created = randomSessionId()
    window.sessionStorage.setItem(TAB_SESSION_ID_KEY, created)
    return created
  } catch {
    return randomSessionId()
  }
}

type InitProjectLockParams = {
  projectId: string
  onConflict: (conflicted: boolean) => void
}

export const initProjectSessionLock = (params: InitProjectLockParams) => {
  const projectId = String(params.projectId || '').trim()
  const onConflict = params.onConflict
  let sessionId = getOrCreateTabSessionId()
  const key = lockKeyOf(projectId)
  let conflicted = false
  let heartbeatTimer: ReturnType<typeof setInterval> | null = null
  let bc: BroadcastChannel | null = null
  let released = false

  const emitConflict = (next: boolean) => {
    if (conflicted === next) return
    conflicted = next
    debugLock('emit conflict', { projectId, sessionId, runtimeInstanceId, conflicted: next })
    onConflict(next)
  }

  const writeHeartbeat = () => {
    if (conflicted || released) return
    const payload: LockPayload = { sessionId, projectId, updatedAt: now() }
    localStorage.setItem(key, JSON.stringify(payload))
    debugLock('write heartbeat', { projectId, sessionId, runtimeInstanceId, key })
    bc?.postMessage({ type: 'heartbeat', projectId, sessionId })
  }

  const clearOwnLock = () => {
    const current = parseLockPayload(localStorage.getItem(key))
    if (current?.sessionId === sessionId) {
      localStorage.removeItem(key)
      debugLock('clear own lock', { projectId, sessionId, runtimeInstanceId, key })
    }
    bc?.postMessage({ type: 'release', projectId, sessionId })
  }

  const detectConflict = () => {
    const current = parseLockPayload(localStorage.getItem(key))
    debugLock('detect conflict', { projectId, sessionId, runtimeInstanceId, current, expired: current ? isExpired(current) : null })
    if (!current || isExpired(current)) {
      emitConflict(false)
      writeHeartbeat()
      return
    }
    console.log(sessionId)
    emitConflict(current.sessionId !== sessionId)
  }

  const onStorage = (event: StorageEvent) => {
    if (event.key !== key) return
    detectConflict()
  }

  const onBeforeUnload = () => {
    clearOwnLock()
  }

  const startHeartbeat = () => {
    if (heartbeatTimer || conflicted || released) return
    heartbeatTimer = setInterval(writeHeartbeat, HEARTBEAT_MS)
  }

  if (typeof BroadcastChannel !== 'undefined') {
    bc = new BroadcastChannel(CHANNEL_NAME)
    bc.onmessage = (event: MessageEvent<{
      type?: string
      projectId?: string
      sessionId?: string
      candidateSessionId?: string
      runtimeId?: string
      responderRuntimeId?: string
      targetRuntimeId?: string
    }>) => {
      const payload = event.data
      if (!payload || payload.projectId !== projectId) return
      if (payload.type === 'session-probe') {
        if (payload.candidateSessionId !== sessionId) return
        if (payload.runtimeId === runtimeInstanceId) return
        debugLock('session probe received', {
          projectId,
          sessionId,
          runtimeInstanceId,
          fromRuntimeId: payload.runtimeId
        })
        bc?.postMessage({
          type: 'session-probe-ack',
          projectId,
          candidateSessionId: sessionId,
          responderRuntimeId: runtimeInstanceId,
          targetRuntimeId: payload.runtimeId
        })
        return
      }
      if (payload.type === 'session-probe-ack') {
        return
      }
      if (payload.sessionId === sessionId) return
      if (payload.type === 'heartbeat') {
        detectConflict()
      }
    }
  }

  const ensureUniqueSessionId = async () => {
    if (!bc) return
    const responders = new Set<string>()
    const onMessage = (event: MessageEvent<{
      type?: string
      projectId?: string
      candidateSessionId?: string
      responderRuntimeId?: string
      targetRuntimeId?: string
    }>) => {
      const payload = event.data
      if (!payload || payload.type !== 'session-probe-ack') return
      if (payload.projectId !== projectId) return
      if (payload.candidateSessionId !== sessionId) return
      if (payload.targetRuntimeId !== runtimeInstanceId) return
      if (payload.responderRuntimeId) {
        responders.add(payload.responderRuntimeId)
      }
    }
    bc.addEventListener('message', onMessage as EventListener)
    try {
      bc.postMessage({
        type: 'session-probe',
        projectId,
        candidateSessionId: sessionId,
        runtimeId: runtimeInstanceId
      })
      await new Promise(resolve => window.setTimeout(resolve, SESSION_PROBE_WAIT_MS))
    } finally {
      bc.removeEventListener('message', onMessage as EventListener)
    }
    if (released || responders.size === 0) return
    const shouldRotate = Array.from(responders).some(responderRuntimeId => responderRuntimeId < runtimeInstanceId)
    debugLock('session probe result', {
      projectId,
      sessionId,
      runtimeInstanceId,
      responders: Array.from(responders),
      shouldRotate
    })
    if (!shouldRotate) return
    const nextSessionId = randomSessionId()
    try {
      window.sessionStorage.setItem(TAB_SESSION_ID_KEY, nextSessionId)
    } catch {
      // ignore sessionStorage write errors
    }
    debugLock('rotate duplicated session id', {
      projectId,
      previousSessionId: sessionId,
      nextSessionId,
      runtimeInstanceId
    })
    sessionId = nextSessionId
  }

  void (async () => {
    try {
      await ensureUniqueSessionId()
      if (released) return
      const existing = parseLockPayload(localStorage.getItem(key))
      debugLock('init lock state', { projectId, sessionId, runtimeInstanceId, existing })
      if (existing && !isExpired(existing) && existing.sessionId !== sessionId) {
        emitConflict(true)
      } else {
        writeHeartbeat()
      }
      startHeartbeat()
    } catch (error) {
      console.error('init project session lock failed:', error)
    }
  })()

  window.addEventListener('storage', onStorage)
  window.addEventListener('beforeunload', onBeforeUnload)

  return {
    get conflicted() {
      return conflicted
    },
    release: () => {
      released = true
      if (heartbeatTimer) {
        clearInterval(heartbeatTimer)
        heartbeatTimer = null
      }
      window.removeEventListener('storage', onStorage)
      window.removeEventListener('beforeunload', onBeforeUnload)
      clearOwnLock()
      if (bc) {
        bc.close()
        bc = null
      }
    }
  }
}

export const hasActiveProjectSessionLock = (projectIdRaw: string) => {
  const projectId = String(projectIdRaw || '').trim()
  if (!projectId) return false
  try {
    const payload = parseLockPayload(localStorage.getItem(lockKeyOf(projectId)))
    return Boolean(payload && !isExpired(payload))
  } catch {
    return false
  }
}

export const collectActiveProjectSessionLocks = (projectIds: string[]) => {
  const result = new Set<string>()
  for (const projectId of projectIds) {
    if (hasActiveProjectSessionLock(projectId)) {
      result.add(String(projectId || '').trim())
    }
  }
  return result
}
