import Foundation

/// A file or piece of media the user attached to a composer message, carried from the UI
/// down to the point where it's written into the session's working tree. Just bytes plus a
/// display name — the UI has already read the file (or captured the pasted/dragged image
/// data) before handing it over, so this stays `Sendable` and free of any view types.
public struct PendingAttachment: Sendable, Equatable {
    /// The original (or synthesized) file name, e.g. `screenshot.png`. Sanitized before it
    /// hits disk — never trusted as a path.
    public let filename: String
    public let data: Data

    public init(filename: String, data: Data) {
        self.filename = filename
        self.data = data
    }
}

/// Materializes composer attachments into a session's working tree and builds the message
/// reference block that points the agent at them.
///
/// Why files-on-disk rather than inline image blocks: it's backend-agnostic (Claude, Codex,
/// and Grok all just read the working tree), it survives the container sandbox (the worktree
/// is bind-mounted at its identical host path, so a path written here resolves inside the
/// guest), and it needs no change to the `AgentInput` wire format. The agent reaches the
/// files with its ordinary `Read` tool, which renders images visually.
public enum AttachmentMaterializer {
    /// Where attachments land, relative to the working-tree root. `.nucleic/` is already in
    /// the worktree's git exclude (see `WorktreeManager.excludeNucleicDir`), so dropped files
    /// never show up as changes to integrate.
    public static let directory = ".nucleic/attachments"

    /// Write each attachment under `<worktree>/.nucleic/attachments/<token>-<name>` and return
    /// the paths *relative to the worktree root* (the agent's working directory), in order.
    /// A per-file random token prevents same-named files from colliding within a turn.
    @discardableResult
    public static func materialize(
        _ attachments: [PendingAttachment], intoWorktree worktree: String
    ) throws -> [String] {
        guard !attachments.isEmpty else { return [] }
        let baseURL = URL(fileURLWithPath: worktree, isDirectory: true)
            .appendingPathComponent(directory, isDirectory: true)
        try FileManager.default.createDirectory(at: baseURL, withIntermediateDirectories: true)

        var relativePaths: [String] = []
        for attachment in attachments {
            let safeName = sanitizeFilename(attachment.filename)
            let token = String(UUID().uuidString.prefix(8)).lowercased()
            let storedName = "\(token)-\(safeName)"
            let dest = baseURL.appendingPathComponent(storedName)
            try attachment.data.write(to: dest, options: .atomic)
            relativePaths.append("\(directory)/\(storedName)")
        }
        return relativePaths
    }

    /// Fold the attachment references into the user's message so the agent sees what was
    /// attached and where to read it. Appended after the user's own text (or standing alone
    /// when the user attached files without typing anything).
    public static func augment(message: String, withReferencePaths paths: [String]) -> String {
        guard !paths.isEmpty else { return message }
        let list = paths.map { "- \($0)" }.joined(separator: "\n")
        let noun = paths.count == 1 ? "file" : "files"
        let header = message.isEmpty
            ? "Please take a look at the attached \(noun) (in the working tree):"
            : "Attached \(noun) (in the working tree):"
        let block = "\(header)\n\(list)"
        return message.isEmpty ? block : message + "\n\n" + block
    }

    /// Reduce an arbitrary file name to a safe single path component: keep it to the last path
    /// component, allow only a conservative character set (others become `_`), and cap the
    /// length so a pathological name can't blow out the path. Never empty.
    static func sanitizeFilename(_ raw: String) -> String {
        let lastComponent = (raw as NSString).lastPathComponent
        let allowed = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "._-()[]"))
        var cleaned = String(lastComponent.unicodeScalars.map {
            allowed.contains($0) ? Character($0) : "_"
        })
        // A name that's all dots ("." / "..") or empty would be unusable as a file.
        if cleaned.allSatisfy({ $0 == "." }) || cleaned.isEmpty { cleaned = "attachment" }
        if cleaned.count > 120 { cleaned = String(cleaned.suffix(120)) }
        return cleaned
    }
}
