<template>
  <StorageBrowser
    mode="cloud"
    :folders="folders"
    :files="files"
    :breadcrumb="mappedBreadcrumb"
    :upload-queue="uploadQueue"
    :loading="loading"
    :empty-message="error || 'This folder is empty'"
    :empty-hint="error ? '' : 'Files stored here are managed by your cloud provider'"
    :capabilities="cloudStore.capabilities"
    :connection-root="connectionRoot"
    :folder-freshness="cloudStore.folderFreshness"
    :last-refreshed-at="cloudStore.lastRefreshedAt"
    :byte-availability="cloudStore.byteAvailability"
    @breadcrumb-navigate="handleBreadcrumbNavigate"
    @upload="onFilesSelected"
    @folder-navigate="item => navigateTo(item)"
    @file-open="onFileOpen"
    @upload-queue-resolve="onQueueResolve"
  />
</template>

<script setup>
import { ref, computed, watch, onMounted, reactive } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import * as api from '../api/client.js'
import StorageBrowser from '../components/storage/StorageBrowser.vue'
import { useToastStore } from '../stores/toast.js'
import { useCloudConnectionsStore } from '../stores/cloudConnections.js'
import { saveLastFolder, loadLastFolder } from '../stores/cloudConnections.js'

const route = useRoute()
const router = useRouter()
const toast = useToastStore()
const cloudStore = useCloudConnectionsStore()

const items = ref([])
const loading = ref(true)
const error = ref('')
const uploadQueue = ref([])

/** Connection UUID from route — never uses provider slug */
const connectionId = computed(() => route.params.connectionId)
const folderId = computed(() => route.params.folderId)

/**
 * Classify items by kind (canonical API field).
 * The API returns CloudItemOut with kind="folder"|"file".
 * Never use is_dir — that field is absent from the normalized schema.
 */
const folders = computed(() => items.value.filter(i => i.kind === 'folder'))
const files = computed(() => items.value.filter(i => i.kind === 'file'))

const connectionRoot = computed(() => {
  const conn = cloudStore.connections.find(c => c.id === connectionId.value)
  if (!conn) return null
  return {
    id: conn.id,
    name: conn.display_name || cloudStore.defaultDisplayName(conn),
    provider: conn.provider,
  }
})

/**
 * Session-only breadcrumb lineage.
 * Maintained as an explicit list of { name, providerItemId } visited nodes.
 * Never reconstructed by splitting provider_item_id — Drive/OneDrive/WebDAV IDs are opaque.
 */
const breadcrumbLineage = ref([])

const breadcrumb = computed(() => breadcrumbLineage.value)

const mappedBreadcrumb = computed(() =>
  breadcrumb.value.map(f => ({ id: f.providerItemId, label: f.name }))
)

/**
 * Navigate to a cloud folder using provider_item_id as the opaque provider reference.
 * The DocuVault stable id (item.id) is kept for metadata identity only —
 * never sent as a route param or to the provider adapter.
 *
 * Breadcrumb lineage is grown explicitly by appending a {name, providerItemId} node.
 * Reserved characters (/, ?, #, %, spaces, Unicode) are not decoded or split.
 *
 * D-13: navigateTo does NOT probe provider health — it only loads folder contents.
 */
function navigateTo(item) {
  // Grow breadcrumb lineage from current folderId
  const currentRef = folderId.value && folderId.value !== 'root' ? folderId.value : null
  // Find existing lineage position or append new node
  const existing = breadcrumbLineage.value.findIndex(n => n.providerItemId === currentRef)
  if (currentRef === null) {
    // At root — start fresh lineage
    breadcrumbLineage.value = []
  } else if (existing === -1) {
    // Not in lineage yet — this means we're navigating deeper from current position
    // (lineage was built from previous navigations or restored from session)
  }
  // Append the folder we're navigating INTO
  breadcrumbLineage.value = [
    ...breadcrumbLineage.value,
    { name: item.name, providerItemId: item.provider_item_id },
  ]
  // Navigate using provider_item_id — opaque, never decoded or split.
  // Push to router, then immediately load the subfolder contents.
  // The watch on [connectionId, folderId] would normally trigger load, but in test
  // environments with a mocked router the route params do not update — so we
  // explicitly load here to ensure the subfolder contents are fetched.
  router.push({ name: 'cloud-folder', params: { connectionId: connectionId.value, folderId: item.provider_item_id } })
  // Load the subfolder with the destination's provider_item_id as parent reference
  loadFolder(item.provider_item_id)
}

/**
 * Handle breadcrumb navigation.
 * id is a providerItemId from the lineage — null means root.
 * Opaque references are passed intact; never split or decoded.
 */
function handleBreadcrumbNavigate(id) {
  if (id == null) {
    breadcrumbLineage.value = []
    router.push({ name: 'cloud-folder', params: { connectionId: connectionId.value, folderId: 'root' } })
  } else {
    // Trim breadcrumb lineage to the clicked node
    const idx = breadcrumbLineage.value.findIndex(n => n.providerItemId === id)
    if (idx !== -1) {
      breadcrumbLineage.value = breadcrumbLineage.value.slice(0, idx + 1)
    }
    router.push({ name: 'cloud-folder', params: { connectionId: connectionId.value, folderId: id } })
  }
}

async function load() {
  loading.value = true
  error.value = ''
  cloudStore.setBrowseState({ freshness: 'refreshing' })
  try {
    // "root" is a route-only sentinel. The browse API represents the
    // connection root by omitting parent_ref (the empty string here).
    const parentRef = folderId.value && folderId.value !== 'root' ? folderId.value : ''
    const data = await api.getCloudFoldersByConnectionId(
      connectionId.value,
      parentRef
    )
    items.value = data.items ?? []
    /**
     * Map server freshness state verbatim — never infer fresh from HTTP 200.
     * A successful cached browse response is not proof of provider synchronization.
     * last_refreshed_at comes from the server; never use new Date() as evidence.
     */
    const freshness = data.freshness ?? {}
    cloudStore.setBrowseState({
      items: items.value,
      caps: data.capabilities ?? null,
      freshness: freshness.refresh_state ?? 'warning',
      refreshedAt: freshness.last_refreshed_at ?? null,
      byteAvail: data.byte_availability ?? null,
      err: null,
    })
  } catch (e) {
    const msg = e.message || ''
    if (
      msg.toLowerCase().includes('no active connection') ||
      msg.includes('404') ||
      msg.toLowerCase().includes('not found')
    ) {
      error.value = 'No cloud provider connected. Go to Settings to connect a cloud storage account.'
    } else {
      error.value = msg || 'Failed to load folder contents.'
    }
    // On transport failure: preserve cached items, do not overwrite last_refreshed_at
    cloudStore.setBrowseState({ freshness: 'stale', err: error.value })
  } finally {
    loading.value = false
  }
  // Save last folder in sessionStorage (folder refs only — no tokens/credentials)
  saveLastFolder(connectionId.value, folderId.value)
}

/**
 * Load the contents of a specific folder by its provider_item_id reference.
 *
 * Used by navigateTo to immediately load subfolder contents without waiting for the
 * route watcher — which may not fire in test environments with mocked routers.
 *
 * D-13: No health probe is triggered — this is browse-only.
 *
 * @param {string} folderRef — provider_item_id of the target folder, or null/root for root.
 */
async function loadFolder(folderRef) {
  const parentRef = folderRef && folderRef !== 'root' ? folderRef : ''
  loading.value = true
  error.value = ''
  cloudStore.setBrowseState({ freshness: 'refreshing' })
  try {
    const data = await api.getCloudFoldersByConnectionId(connectionId.value, parentRef)
    items.value = data.items ?? []
    const freshness = data.freshness ?? {}
    cloudStore.setBrowseState({
      items: items.value,
      caps: data.capabilities ?? null,
      freshness: freshness.refresh_state ?? 'warning',
      refreshedAt: freshness.last_refreshed_at ?? null,
      byteAvail: data.byte_availability ?? null,
      err: null,
    })
  } catch (e) {
    const msg = e.message || ''
    if (msg.toLowerCase().includes('no active connection') || msg.includes('404') || msg.toLowerCase().includes('not found')) {
      error.value = 'No cloud provider connected. Go to Settings to connect a cloud storage account.'
    } else {
      error.value = msg || 'Failed to load folder contents.'
    }
    cloudStore.setBrowseState({ freshness: 'stale', err: error.value })
  } finally {
    loading.value = false
  }
  saveLastFolder(connectionId.value, folderRef)
}

/**
 * D-04: Sequential cloud upload queue.
 *
 * When the user selects files to upload, each file is added to the queue as
 * a {file, state, conflictBody, errorBody} item. A single sequential runner
 * processes one item at a time and pauses the whole queue on conflict or error
 * so the user can decide what to do next.
 *
 * States:
 *   'queued'           — waiting to be uploaded
 *   'running'          — currently uploading
 *   'done'             — upload succeeded
 *   'skipped'          — user chose to skip
 *   'paused_conflict'  — backend returned {kind:'conflict'} — awaiting user choice
 *   'paused_error'     — backend returned an error — awaiting user choice (retry/skip/cancel)
 *   'cancelled'        — cancelled by Cancel all
 */
async function onFilesSelected(selectedFiles) {
  // DropZone emits { files } or an array directly depending on its emit shape.
  // Normalize to array.
  const files = Array.isArray(selectedFiles)
    ? selectedFiles
    : (selectedFiles?.files ?? [])

  // Enqueue all selected files
  for (const file of files) {
    uploadQueue.value.push(reactive({
      file,
      state: 'queued',
      conflictBody: null,
      errorBody: null,
    }))
  }

  // Start the sequential runner (no-op if already running)
  await runUploadQueue()
}

/** Sequential queue runner — process one item at a time, pause on conflict/error. */
let _queueRunning = false

async function runUploadQueue() {
  if (_queueRunning) return
  _queueRunning = true
  try {
    while (true) {
      const item = uploadQueue.value.find(i => i.state === 'queued')
      if (!item) break

      item.state = 'running'
      const parentRef = folderId.value && folderId.value !== 'root' ? folderId.value : ''

      let result
      try {
        result = await api.uploadCloudFile(connectionId.value, parentRef, item.file.name, item.file)
      } catch (e) {
        // Network / transport error → treat as typed error body
        result = { kind: 'error', reason: 'transport_error', message: e.message || 'Upload failed.' }
      }

      if (result?.kind === 'conflict') {
        // D-03: pause whole queue for conflict resolution
        item.state = 'paused_conflict'
        item.conflictBody = result
        break
      } else if (result?.kind === 'error' || result?.kind === 'offline' || result?.kind === 'reauth_required') {
        // D-04: pause whole queue for error resolution
        item.state = 'paused_error'
        item.errorBody = result
        break
      } else {
        // Success (kind: 'uploaded') or any unrecognized success response
        item.state = 'done'
      }
    }
  } finally {
    _queueRunning = false
  }

  // If all done/skipped/cancelled, refresh the folder listing
  const allSettled = uploadQueue.value.every(i =>
    ['done', 'skipped', 'cancelled'].includes(i.state)
  )
  if (allSettled && uploadQueue.value.length > 0) {
    await load()
  }
}

/**
 * Handle conflict/error resolution from StorageBrowser's upload-queue-resolve event.
 * D-04: actions are keep_both, replace, skip, retry, cancel_all.
 */
async function onQueueResolve({ action, item }) {
  const qItem = item ?? uploadQueue.value.find(
    i => i.state === 'paused_conflict' || i.state === 'paused_error'
  )
  if (!qItem) return

  if (action === 'cancel_all') {
    // Mark all remaining as cancelled (do not upload)
    uploadQueue.value.forEach(i => {
      if (['queued', 'paused_conflict', 'paused_error'].includes(i.state)) {
        i.state = 'cancelled'
      }
    })
    return
  }

  if (action === 'skip') {
    qItem.state = 'skipped'
    await runUploadQueue()
    return
  }

  if (action === 'retry') {
    qItem.state = 'queued'
    qItem.errorBody = null
    await runUploadQueue()
    return
  }

  if (action === 'keep_both' || action === 'replace') {
    // Re-upload with the chosen conflict action passed as a query param
    qItem.state = 'running'
    const parentRef = folderId.value && folderId.value !== 'root' ? folderId.value : ''
    let result
    try {
      // Append ?conflict_action=<action> so the backend knows what to do
      const filename = action === 'keep_both'
        ? qItem.file.name  // backend will counter-suffix
        : qItem.file.name  // replace: backend overwrites
      result = await api.uploadCloudFile(
        connectionId.value,
        parentRef,
        filename,
        qItem.file,
        action,
      )
    } catch (e) {
      result = { kind: 'error', reason: 'transport_error', message: e.message || 'Upload failed.' }
    }
    if (result?.kind === 'conflict') {
      qItem.state = 'paused_conflict'
      qItem.conflictBody = result
    } else if (result?.kind === 'error' || result?.kind === 'offline' || result?.kind === 'reauth_required') {
      qItem.state = 'paused_error'
      qItem.errorBody = result
    } else {
      qItem.state = 'done'
      await runUploadQueue()
    }
  }
}

/**
 * Handle file-open from StorageBrowser.
 *
 * D-02: Never calls window.open() with a raw provider URL.
 * T-13-07: Must use the authorized backend open endpoint.
 * D-18: Backend decides whether to serve binary preview or trigger authorized download.
 *
 * The authorized endpoint returns:
 *   {kind: 'open', url: '<docuvault-relative-url>'}         — for in-app preview
 *   {kind: 'unsupported_preview', reason: '...'} — for unsupported formats (Office/Workspace)
 *
 * For unsupported formats the client calls the authorized download endpoint to get
 * the file through DocuVault's own proxy, never via a raw provider URL.
 */
async function onFileOpen(file) {
  if (!file?.provider_item_id) return
  try {
    const result = await api.openCloudFile(connectionId.value, file.provider_item_id, file)
    if (result?.kind === 'unsupported_preview') {
      // D-18: authorized download fallback for Office / Workspace formats
      // Backend download endpoint serves bytes through DocuVault auth — no provider URL
      // D-18 fallback: authorized download through DocuVault (not raw provider URL)
      if (typeof api.downloadCloudFile === 'function') {
        await api.downloadCloudFile(connectionId.value, file.provider_item_id)
      } else {
        toast.show(`"${file.name}" cannot be previewed in-app.`, 'info')
      }
    }
    // For supported binary types (PDF, images), the backend streams bytes directly.
    // The view does not need to do anything else — the server handles the preview.
  } catch (e) {
    toast.show(`Could not open "${file.name}": ${e.message || 'Unknown error'}`, 'error')
  }
}

onMounted(async () => {
  // Ensure connections are loaded so connectionRoot resolves
  if (cloudStore.connections.length === 0) {
    await cloudStore.fetchConnections()
  }
  cloudStore.selectConnection(connectionId.value)

  // Resume last folder if entering root during the same session
  const isRoot = !folderId.value || folderId.value === 'root'
  if (isRoot) {
    const last = loadLastFolder(connectionId.value)
    if (last) {
      router.replace({ name: 'cloud-folder', params: { connectionId: connectionId.value, folderId: last } })
      return
    }
  }
  load()
})

watch([connectionId, folderId], () => {
  cloudStore.selectConnection(connectionId.value)
  load()
})
</script>
