<template>
  <div class="pb-6">
    <FederationHeader
      :self-did="selfDid"
      :server-name="appStore.serverName"
      @rotate="showRotateModal = true"
    />

    <RotateDidModal
      :visible="showRotateModal"
      :rotating="rotatingDid"
      :error="rotateError"
      :success="rotateSuccess"
      @close="showRotateModal = false; rotateError = ''; rotateSuccess = ''"
      @rotate="rotateDid"
    />

    <!-- View Tabs -->
    <div v-if="nodes.length > 0" class="flex gap-1 mb-6 p-1 bg-black/20 rounded-lg w-fit">
      <button
        v-for="tab in viewTabs"
        :key="tab.id"
        class="px-4 py-2 rounded text-sm font-medium transition-colors"
        :class="activeView === tab.id ? 'bg-white/10 text-white border-b-2 border-orange-400' : 'text-white/50 hover:text-white/70'"
        @click="setView(tab.id)"
      >
        {{ tab.label }}
      </button>
    </div>

    <!-- Network Map View -->
    <div v-if="activeView === 'map' && nodes.length > 0" class="mb-6">
      <NetworkMap :nodes="mapNodes" :links="mapLinks" />
    </div>

    <template v-if="activeView === 'list'">
      <QuickActions
        :generating-invite="generatingInvite"
        :invite-type="inviteType"
        :invite-code="inviteCode"
        :syncing="syncing"
        @generate-invite="handleGenerateInvite"
        @show-join="showJoinModal = true"
        @sync="syncAll"
        @clear-invite="inviteCode = ''"
      />

      <!-- Nostr discoverability strip: opt-in toggle + Discover button.
           Renders inline so the Federation page is the single place a user
           manages everything related to peering. The toggle directly mutates
           the `nostr_discovery_enabled` config flag — backend defaults to OFF
           and nothing is published until the user explicitly turns it on. -->
      <div class="glass-card p-4 mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
        <div class="min-w-0">
          <div class="flex items-center gap-2">
            <span class="text-sm font-medium text-white">Nostr discoverability</span>
            <span
              class="inline-block px-2 py-0.5 text-[10px] uppercase tracking-wide rounded"
              :class="discoveryEnabled ? 'bg-green-500/20 text-green-300' : 'bg-white/10 text-white/50'"
            >{{ discoveryEnabled ? 'On' : 'Off' }}</span>
          </div>
          <p class="text-xs text-white/60 mt-1">
            When on, this node publishes a presence event (DID + npub only — never an onion)
            so other nodes can find you and request to peer. Inbound requests land in the
            panel below for your approval. Off by default.
          </p>
        </div>
        <div class="flex items-center gap-2 shrink-0">
          <button
            class="px-3 py-1.5 glass-button glass-button-sm rounded text-xs text-white/90 hover:text-white disabled:opacity-50"
            :disabled="discoveryToggling"
            @click="toggleDiscovery"
          >
            {{ discoveryToggling ? '…' : (discoveryEnabled ? 'Disable' : 'Enable') }}
          </button>
          <button
            class="px-3 py-1.5 glass-button glass-button-sm rounded text-xs text-white/90 hover:text-white disabled:opacity-50"
            :disabled="!discoveryEnabled"
            @click="showDiscoverModal = true"
          >
            Discover Nodes
          </button>
        </div>
      </div>
      <div v-if="discoveryError" class="mb-4 text-xs text-red-400">{{ discoveryError }}</div>

      <PendingRequestsPanel
        :requests="pendingRequests"
        :polling="pollingHandshake"
        :busy-id="pendingBusyId"
        @poll="pollHandshake"
        @approve="approvePending"
        @reject="rejectPending"
      />

      <NodeList
        :nodes="nodes"
        :loading="loading"
        :error="error"
        :sync-results="syncResults"
        :dwn-sync-dot-class="dwnSyncDotClass"
        :cleaning-nodes="cleaningNodes"
        @select-node="selectedNode = $event"
        @clear-sync-results="syncResults = []"
        @cleanup-dead="cleanupDeadNodes"
      />
    </template>

    <NodeDetailModal
      :node="selectedNode"
      :dwn-sync-dot-class="dwnSyncDotClass"
      :dwn-sync-label="dwnSyncLabel"
      :dwn-message-count="String(dwnStatus?.message_count ?? '--')"
      :dwn-last-sync="dwnStatus?.last_sync ? timeAgo(dwnStatus.last_sync) : 'never'"
      :dwn-syncing="dwnSyncing"
      :deploying="deploying"
      :deploy-result="deployResult"
      @close="selectedNode = null"
      @change-trust="changeTrust"
      @remove-node="removeNode"
      @deploy-app="deployApp"
      @dwn-sync="triggerDwnSync"
    />

    <JoinModal
      :visible="showJoinModal"
      :joining="joining"
      :error="joinError"
      :success="joinSuccess"
      @close="showJoinModal = false"
      @join="joinFederation"
    />

    <DiscoverModal
      :visible="showDiscoverModal"
      :outbound-sent="pendingRequests"
      @close="showDiscoverModal = false"
      @sent="loadPendingRequests"
    />
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { rpcClient } from '@/api/rpc-client'
import { useTransportStore } from '@/stores/transport'
import { useAppStore } from '@/stores/app'
import NetworkMap from '@/components/federation/NetworkMap.vue'
import FederationHeader from './federation/FederationHeader.vue'
import RotateDidModal from './federation/RotateDidModal.vue'
import QuickActions from './federation/QuickActions.vue'
import NodeList from './federation/NodeList.vue'
import NodeDetailModal from './federation/NodeDetailModal.vue'
import JoinModal from './federation/JoinModal.vue'
import PendingRequestsPanel from './federation/PendingRequestsPanel.vue'
import DiscoverModal from './federation/DiscoverModal.vue'
import type { FederatedNode, DwnStatus, SyncResult } from './federation/types'
import type { PendingPeerRequest } from '@/api/rpc-client'
import { nodeName, timeAgo } from './federation/utils'

const transportStore = useTransportStore()
const appStore = useAppStore()

const nodes = ref<FederatedNode[]>([])
const loading = ref(true)
const error = ref('')
const selectedNode = ref<FederatedNode | null>(null)
const inviteType = ref<'trusted' | 'observer'>('trusted')

const inviteCode = ref('')
const generatingInvite = ref(false)

const showJoinModal = ref(false)
const joining = ref(false)
const joinError = ref('')
const joinSuccess = ref(false)

const syncing = ref(false)
const syncResults = ref<SyncResult[]>([])

const deploying = ref(false)
const deployResult = ref('')

const viewTabs = [
  { id: 'list', label: 'List View' },
  { id: 'map', label: 'Network Map' },
] as const

type ViewId = typeof viewTabs[number]['id']
const activeView = ref<ViewId>(
  (localStorage.getItem('federation-view') as ViewId) || (nodes.value.length >= 3 ? 'map' : 'list')
)

function setView(id: ViewId) {
  activeView.value = id
  localStorage.setItem('federation-view', id)
}

const selfDid = ref('')

const mapNodes = computed(() => {
  const result = []
  if (selfDid.value) {
    result.push({
      did: selfDid.value,
      label: appStore.serverName,
      trust_level: 'trusted' as const,
      online: true,
      app_count: 0,
      is_self: true,
    })
  }
  for (const node of nodes.value) {
    result.push({
      did: node.did,
      label: nodeName(node),
      trust_level: node.trust_level as 'trusted' | 'observer' | 'untrusted',
      online: isOnlineCheck(node),
      app_count: node.last_state?.apps?.length ?? 0,
      is_self: false,
    })
  }
  return result
})

const mapLinks = computed(() => {
  if (!selfDid.value) return []
  return nodes.value.map(n => ({
    source: selfDid.value,
    target: n.did,
  }))
})

const dwnStatus = ref<DwnStatus | null>(null)
const dwnSyncing = ref(false)

const dwnSyncDotClass = computed(() => {
  if (!dwnStatus.value) return 'bg-white/30'
  switch (dwnStatus.value.sync_status) {
    case 'synced': return 'bg-green-400'
    case 'syncing': return 'bg-yellow-400 animate-pulse'
    case 'error': return 'bg-red-400'
    default: return 'bg-white/30'
  }
})

const dwnSyncLabel = computed(() => {
  if (!dwnStatus.value) return 'Unknown'
  switch (dwnStatus.value.sync_status) {
    case 'synced': return 'Synced'
    case 'syncing': return 'Syncing...'
    case 'error': return 'Error'
    default: return dwnStatus.value.sync_status
  }
})

// DID rotation
const showRotateModal = ref(false)
const rotatingDid = ref(false)
const rotateError = ref('')
const rotateSuccess = ref('')

// Dead node cleanup
const cleaningNodes = ref(false)

// Nostr discoverability + pending peer requests
const discoveryEnabled = ref(false)
const discoveryToggling = ref(false)
const discoveryError = ref('')
const showDiscoverModal = ref(false)
const pendingRequests = ref<PendingPeerRequest[]>([])
const pollingHandshake = ref(false)
const pendingBusyId = ref<string | null>(null)

async function loadDiscoveryState() {
  try {
    const result = await rpcClient.nostrDiscoveryStatus()
    discoveryEnabled.value = !!result.enabled
  } catch {
    discoveryEnabled.value = false
  }
}

async function toggleDiscovery() {
  discoveryToggling.value = true
  discoveryError.value = ''
  const next = !discoveryEnabled.value
  try {
    const result = await rpcClient.nostrSetDiscovery(next)
    discoveryEnabled.value = !!result.enabled
  } catch (e: unknown) {
    discoveryError.value = e instanceof Error ? e.message : 'Failed to toggle discoverability'
  } finally {
    discoveryToggling.value = false
  }
}

async function loadPendingRequests() {
  try {
    const result = await rpcClient.federationListPendingRequests()
    pendingRequests.value = result.requests
  } catch (e: unknown) {
    discoveryError.value = e instanceof Error ? e.message : 'Failed to load pending requests'
  }
}

async function pollHandshake() {
  pollingHandshake.value = true
  discoveryError.value = ''
  try {
    await rpcClient.handshakePoll()
    await loadPendingRequests()
    // If a poll applied a PeerInvite, the federation node list also changed.
    await loadNodes()
  } catch (e: unknown) {
    discoveryError.value = e instanceof Error ? e.message : 'Poll failed'
  } finally {
    pollingHandshake.value = false
  }
}

async function approvePending(id: string) {
  pendingBusyId.value = id
  discoveryError.value = ''
  try {
    await rpcClient.federationApproveRequest(id)
    await loadPendingRequests()
    await loadNodes()
  } catch (e: unknown) {
    discoveryError.value = e instanceof Error ? e.message : 'Approve failed'
  } finally {
    pendingBusyId.value = null
  }
}

async function rejectPending(id: string) {
  pendingBusyId.value = id
  discoveryError.value = ''
  try {
    await rpcClient.federationRejectRequest(id)
    await loadPendingRequests()
  } catch (e: unknown) {
    discoveryError.value = e instanceof Error ? e.message : 'Reject failed'
  } finally {
    pendingBusyId.value = null
  }
}

function isOnlineCheck(node: FederatedNode): boolean {
  if (!node.last_seen) return false
  const lastSeen = new Date(node.last_seen).getTime()
  const tenMinutesAgo = Date.now() - 10 * 60 * 1000
  return lastSeen > tenMinutesAgo
}

async function loadNodes() {
  try {
    loading.value = true
    const result = await rpcClient.federationListNodes()
    nodes.value = result.nodes
  } catch (e) {
    error.value = e instanceof Error ? e.message : 'Failed to load nodes'
  } finally {
    loading.value = false
  }
}

function handleGenerateInvite(type: 'trusted' | 'observer') {
  inviteType.value = type
  generateInvite()
}

async function generateInvite() {
  try {
    generatingInvite.value = true
    error.value = ''
    const result = await rpcClient.federationInvite()
    inviteCode.value = result.code
  } catch (e) {
    error.value = e instanceof Error ? e.message : 'Failed to generate invite'
  } finally {
    generatingInvite.value = false
  }
}

async function joinFederation(code: string) {
  try {
    joining.value = true
    joinError.value = ''
    joinSuccess.value = false
    await rpcClient.federationJoin(code)
    joinSuccess.value = true
    await loadNodes()
    setTimeout(() => { showJoinModal.value = false; joinSuccess.value = false }, 1500)
  } catch (e) {
    joinError.value = e instanceof Error ? e.message : 'Failed to join'
  } finally {
    joining.value = false
  }
}

async function syncAll() {
  try {
    syncing.value = true
    error.value = ''
    syncResults.value = []
    const result = await rpcClient.call<{
      synced: number; failed: number;
      results: SyncResult[]
    }>({ method: 'federation.sync-state', timeout: 180000 })
    syncResults.value = result.results
    await loadNodes()
  } catch (e) {
    error.value = e instanceof Error ? e.message : 'Sync failed — some peers may be unreachable over Tor'
  } finally {
    syncing.value = false
  }
}

async function changeTrust(did: string, level: string) {
  try {
    await rpcClient.federationSetTrust(did, level as 'trusted' | 'observer' | 'untrusted')
    await loadNodes()
    if (selectedNode.value?.did === did) {
      selectedNode.value = nodes.value.find(n => n.did === did) ?? null
    }
  } catch (e) {
    error.value = e instanceof Error ? e.message : 'Failed to update trust level'
  }
}

async function removeNode(did: string) {
  try {
    await rpcClient.federationRemoveNode(did)
    selectedNode.value = null
    await loadNodes()
  } catch (e) {
    error.value = e instanceof Error ? e.message : 'Failed to remove node'
  }
}

async function deployApp(did: string, appId: string) {
  try {
    deploying.value = true
    deployResult.value = ''
    await rpcClient.federationDeployApp({ did, appId })
    deployResult.value = `Successfully deployed ${appId} to remote node`
  } catch (e) {
    deployResult.value = `Error: ${e instanceof Error ? e.message : 'Deploy failed'}`
  } finally {
    deploying.value = false
  }
}

async function loadDwnStatus() {
  try {
    const result = await rpcClient.call<DwnStatus>({ method: 'dwn.status' })
    dwnStatus.value = result
  } catch {
    dwnStatus.value = null
  }
}

async function triggerDwnSync() {
  try {
    dwnSyncing.value = true
    await rpcClient.call({ method: 'dwn.sync', timeout: 120000 })
    await loadDwnStatus()
  } catch {
    // Silently handle sync errors
  } finally {
    dwnSyncing.value = false
  }
}

async function cleanupDeadNodes() {
  cleaningNodes.value = true
  try {
    const deadNodes = nodes.value.filter(n => !isOnlineCheck(n) && (!n.last_seen || n.last_seen === 'never'))
    for (const node of deadNodes) {
      await rpcClient.federationRemoveNode(node.did)
    }
    await loadNodes()
  } catch (e) {
    error.value = e instanceof Error ? e.message : 'Cleanup failed'
  } finally {
    cleaningNodes.value = false
  }
}

async function rotateDid(password: string) {
  if (!password) return
  rotatingDid.value = true
  rotateError.value = ''
  rotateSuccess.value = ''
  try {
    const result = await rpcClient.call<{
      old_did: string; new_did: string; proof_signature: string; proof_message: string
    }>({ method: 'node.rotate-did', params: { password } })

    selfDid.value = result.new_did
    try { localStorage.setItem('neode_did', result.new_did) } catch { /* noop */ }
    rotateSuccess.value = `DID rotated. Notifying peers...`

    const notify = await rpcClient.call<{ notified: number; failed: number }>({
      method: 'federation.notify-did-change',
      params: {
        old_did: result.old_did,
        new_did: result.new_did,
        proof_signature: result.proof_signature,
        proof_message: result.proof_message,
      },
      timeout: 120000,
    })
    rotateSuccess.value = `DID rotated successfully. ${notify.notified} peers notified${notify.failed > 0 ? `, ${notify.failed} failed` : ''}.`
  } catch (err: unknown) {
    rotateError.value = err instanceof Error ? err.message : 'Rotation failed'
  } finally {
    rotatingDid.value = false
  }
}

onMounted(async () => {
  loadNodes()
  loadDwnStatus()
  loadDiscoveryState()
  loadPendingRequests()
  transportStore.fetchPeers()
  try {
    const result = await rpcClient.getNodeDid()
    selfDid.value = result.did
  } catch {
    // Self DID not available
  }
})
</script>
