// ========= 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 {
  mimeTypeFor,
  IOResult,
  PathSpec,
  isMissingPath,
  type ByteSource,
  type CLIDoors,
  type CommandFnResult,
  type FlagView,
} from '@struktoai/mirage-core'
import { parseRfc822, type ParsedRfc822 } from '../../../../core/email/_parse.ts'
import type { EmailConfig } from '../../../../core/email/config.ts'
import { build, readBody, splitAddresses, type Attachment, type Source } from './builder.ts'
import { deliver, saveSentCopy } from './deliver.ts'

const ENC = new TextEncoder()

/** The `to` field both outcomes report, spelled as the header reads. */
function addressList(addresses: ParsedRfc822['to']): string {
  return addresses
    .map((entry) => (entry.name === '' ? entry.email : `${entry.name} <${entry.email}>`))
    .join(', ')
}

/** The command's first operand, or a usage error. */
export function firstText(texts: readonly string[], label: string): string {
  const value = texts[0]
  if (value === undefined) throw new Error(`${label} is required`)
  return value
}

/**
 * Reads --attach files through the workspace dispatcher.
 *
 * An account CLI has no mount of its own; an attachment is an unrelated
 * workspace file, so it is read through the op dispatcher the executor
 * hands every CLI, the same door git reads repositories through. The
 * flag bag carries path values as their resolved virtual-path strings
 * (the python executor upgrades them to PathSpec instead).
 */
async function loadAttachments(
  doors: CLIDoors | undefined,
  paths: readonly string[],
): Promise<Attachment[]> {
  if (paths.length === 0) return []
  const dispatch = doors?.dispatch
  if (dispatch === undefined) {
    throw new Error('--attach needs a workspace to read files from')
  }
  const attachments: Attachment[] = []
  for (const path of paths) {
    let data: unknown
    try {
      ;[data] = await dispatch('read', PathSpec.fromStrPath(path))
    } catch (err) {
      if (isMissingPath(err)) {
        throw new Error(`read attachment ${path}: No such file or directory`)
      }
      throw err
    }
    const trimmed = path.replace(/\/+$/, '')
    const filename = trimmed.slice(trimmed.lastIndexOf('/') + 1) || 'attachment'
    attachments.push({
      filename,
      contentType: mimeTypeFor(filename),
      data: data instanceof Uint8Array ? data : new Uint8Array(data as ArrayBufferLike),
    })
  }
  return attachments
}

/**
 * Assembles a message, then sends it or writes its MIME to stdout.
 *
 * Shared by compose, reply and forward: the three differ only in the
 * source message they derive headers from. Without --send the raw RFC
 * 5322 bytes go to stdout, so a composer chain can pick them up.
 */
export async function route(
  config: EmailConfig,
  fl: FlagView,
  stdin: ByteSource | null,
  source: Source | null,
  doors: CLIDoors | undefined,
): Promise<CommandFnResult> {
  const raw = build(
    {
      sender: fl.asStr('from') ?? config.username,
      to: splitAddresses(fl.asList('to')),
      cc: splitAddresses(fl.asList('cc')),
      bcc: splitAddresses(fl.asList('bcc')),
      subject: fl.asStr('subject') ?? null,
      body: await readBody(fl, stdin),
      signature: fl.asStr('signature') ?? null,
      attachments: await loadAttachments(doors, fl.asList('attach')),
    },
    source,
  )
  const save = fl.asStr('save') ?? null
  if (!fl.asBool('send')) {
    if (save === null) return [raw as ByteSource, new IOResult()]
    // --save without --send files the message and sends nothing, so a
    // refused APPEND means nothing happened at all and may fail loudly:
    // there is no delivered message a retry could duplicate.
    const folder = await saveSentCopy(config, raw, save)
    const filed = await parseRfc822(raw)
    const saved = {
      status: 'saved',
      mailbox: folder,
      to: addressList(filed.to),
      subject: filed.subject,
    }
    return [ENC.encode(JSON.stringify(saved)) as ByteSource, new IOResult()]
  }
  const { parsed, warning } = await deliver(config, raw, save)
  const result = {
    status: 'sent',
    to: addressList(parsed.to),
    subject: parsed.subject,
  }
  return [
    ENC.encode(JSON.stringify(result)) as ByteSource,
    new IOResult({ stderr: warning === '' ? null : ENC.encode(warning) }),
  ]
}
