// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========

import type { FindOptions } from '../../resource/base.ts'
import type { PathSpec } from '../../types.ts'
import type { S3Accessor } from '../../accessor/s3.ts'
import { loadS3Module, rawPathOf, s3Prefix, stripKeyPrefix, withClient } from './_client.ts'
import { rstripSlash } from '../../utils/slash.ts'
import { buildTree, emitStartPath, keep, startBasename } from '../../commands/builtin/findEval.ts'
import { compareCodePoints } from '../../utils/sort.ts'

export async function find(
  accessor: S3Accessor,
  path: PathSpec,
  options: FindOptions = {},
): Promise<string[]> {
  const { ListObjectsV2Command } = await loadS3Module(accessor.config)
  const raw = rawPathOf(path)
  const startName = startBasename(path.virtual)
  const pfx = s3Prefix(raw, accessor.config)
  const rootKey = rstripSlash('/' + stripKeyPrefix(pfx, accessor.config)) || '/'
  const baseDepth = rootKey === '/' ? 0 : (rootKey.match(/\//g) ?? []).length
  const results: string[] = []
  const seenDirs = new Set<string>()
  const seen = { descendant: false, marker: false }
  const empty = options.empty === true
  const tree =
    options.tree ??
    buildTree({
      name: options.name,
      iname: options.iname,
      pathPattern: options.pathPattern,
      type: options.type,
      nameExclude: options.nameExclude,
      orNames: options.orNames,
      empty: options.empty,
    })
  await withClient(accessor.config, async (client) => {
    let continuationToken: string | undefined
    do {
      const input: Record<string, unknown> = {
        Bucket: accessor.config.bucket,
        Prefix: pfx,
      }
      if (continuationToken !== undefined) input.ContinuationToken = continuationToken
      const resp = (await client.send(new ListObjectsV2Command(input))) as {
        Contents?: { Key?: string; Size?: number; LastModified?: Date }[]
        IsTruncated?: boolean
        NextContinuationToken?: string
      }
      for (const obj of resp.Contents ?? []) {
        const key = obj.Key
        if (key === undefined) continue
        if (key === pfx) {
          seen.marker = true
          continue
        }
        seen.descendant = true
        const isDir = key.endsWith('/')
        const fullPath = rstripSlash('/' + stripKeyPrefix(key, accessor.config)) || '/'
        const size = obj.Size ?? 0
        if (isDir) {
          if (seenDirs.has(fullPath)) continue
          seenDirs.add(fullPath)
        }
        const entries: [string, 'f' | 'd'][] = [[fullPath, isDir ? 'd' : 'f']]
        // Implicit directories exist only as key prefixes; synthesize the
        // parent chain so find agrees with readdir on externally-populated
        // buckets.
        let parent = fullPath.slice(0, fullPath.lastIndexOf('/')) || '/'
        while (parent !== rootKey && parent !== '/') {
          if (!seenDirs.has(parent)) {
            seenDirs.add(parent)
            entries.push([parent, 'd'])
          }
          parent = parent.slice(0, parent.lastIndexOf('/')) || '/'
        }
        for (const [ep, kind] of entries) {
          const entryName = ep.split('/').pop() ?? ''
          const depth = (ep.match(/\//g) ?? []).length - baseDepth
          if (
            options.maxDepth !== null &&
            options.maxDepth !== undefined &&
            depth > options.maxDepth
          ) {
            continue
          }
          const isEmpty = !empty ? null : kind === 'd' ? false : size === 0
          if (!keep({ key: ep, name: entryName, kind, depth, isEmpty }, tree, options.minDepth)) {
            continue
          }
          if (options.minSize != null || options.maxSize != null) {
            // Directories count as size 0 for -size (deliberate GNU divergence).
            const effective = kind === 'd' ? 0 : size
            if (options.minSize != null && effective < options.minSize) {
              continue
            }
            if (options.maxSize != null && effective > options.maxSize) {
              continue
            }
          }
          results.push(ep)
        }
      }
      continuationToken = resp.IsTruncated === true ? resp.NextContinuationToken : undefined
    } while (continuationToken !== undefined)
  })
  if (seen.descendant || seen.marker) {
    emitStartPath(results, rootKey, startName, {
      kind: 'd',
      isEmpty: empty ? !seen.descendant : null,
      exists: true,
      tree,
      maxDepth: options.maxDepth,
      minDepth: options.minDepth,
      minSize: options.minSize,
      maxSize: options.maxSize,
    })
  }
  // A file key and a synthesized directory can name the same path (`data/a`
  // alongside `data/a/b.txt`), which these stores allow and a filesystem does
  // not; find prints such a path once.
  return [...new Set(results)].sort(compareCodePoints)
}
