#!/usr/bin/env swift

import AppKit
import Darwin
import Foundation
import Security

private let indexService = "com.rainstudio.local-password-vault.index"
private let secretService = "com.rainstudio.local-password-vault.secret"
private let vaultAccount = NSUserName()
private let defaultClipboardSeconds = 45

private struct VaultFailure: Error, CustomStringConvertible {
    let description: String
}

private struct Entry: Codable {
    var name: String
    var account: String?
    var url: String?
    var notes: String?
    var createdAt: String
    var updatedAt: String
}

private struct VaultIndex: Codable {
    var version = 1
    var entries: [String: Entry] = [:]
}

private struct Arguments {
    let command: String
    var positionals: [String]
    var values: [String: String]
    var flags: Set<String>
}

private func fail(_ message: String) -> VaultFailure {
    VaultFailure(description: message)
}

private func securityMessage(_ status: OSStatus) -> String {
    if let text = SecCopyErrorMessageString(status, nil) as String? {
        return "\(text) (\(status))"
    }
    return "Keychain error \(status)"
}

private func checkedName(_ raw: String) throws -> String {
    let name = raw.trimmingCharacters(in: .whitespacesAndNewlines)
    guard !name.isEmpty else { throw fail("条目名称不能为空") }
    guard name.utf8.count <= 200 else { throw fail("条目名称不能超过 200 字节") }
    return name
}

private func key(for name: String) -> String {
    name.folding(options: [.caseInsensitive, .diacriticInsensitive], locale: .current)
        .lowercased()
}

private func isoTimestamp() -> String {
    let formatter = ISO8601DateFormatter()
    formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
    return formatter.string(from: Date())
}

private func parseArguments(_ raw: [String]) throws -> Arguments {
    guard let command = raw.first else { return Arguments(command: "help", positionals: [], values: [:], flags: []) }
    let valueOptions: Set<String> = ["--account", "--url", "--notes", "--clear-after"]
    let flagOptions: Set<String> = [
        "--yes", "--json", "--show", "--replace-secret", "--secret-clipboard", "--secret-stdin"
    ]
    var positionals: [String] = []
    var values: [String: String] = [:]
    var flags: Set<String> = []
    var index = 1

    while index < raw.count {
        let token = raw[index]
        if valueOptions.contains(token) {
            guard index + 1 < raw.count else { throw fail("\(token) 缺少值") }
            values[token] = raw[index + 1]
            index += 2
        } else if flagOptions.contains(token) {
            flags.insert(token)
            index += 1
        } else if token.hasPrefix("--") {
            throw fail("未知选项：\(token)")
        } else {
            positionals.append(token)
            index += 1
        }
    }

    return Arguments(command: command, positionals: positionals, values: values, flags: flags)
}

private func baseQuery(service: String, account: String) -> [CFString: Any] {
    [
        kSecClass: kSecClassGenericPassword,
        kSecAttrService: service,
        kSecAttrAccount: account,
        kSecAttrSynchronizable: false,
    ]
}

private func readKeychainData(service: String, account: String) throws -> Data? {
    var query = baseQuery(service: service, account: account)
    query[kSecReturnData] = true
    query[kSecMatchLimit] = kSecMatchLimitOne

    var result: CFTypeRef?
    let status = SecItemCopyMatching(query as CFDictionary, &result)
    if status == errSecItemNotFound { return nil }
    guard status == errSecSuccess, let data = result as? Data else {
        throw fail("读取 Keychain 失败：\(securityMessage(status))")
    }
    return data
}

private func addKeychainData(
    service: String,
    account: String,
    data: Data,
    label: String,
    requireConfirmation: Bool
) throws {
    var query = baseQuery(service: service, account: account)
    query[kSecValueData] = data
    query[kSecAttrLabel] = label
    query[kSecAttrDescription] = "Rain Studio 钥匙串密码库"
    query[kSecAttrAccess] = try legacyAccess(label: label, trustCurrentTool: !requireConfirmation)

    let status = SecItemAdd(query as CFDictionary, nil)
    guard status == errSecSuccess else {
        throw fail("写入 Keychain 失败：\(securityMessage(status))")
    }
}

private func legacyAccess(label: String, trustCurrentTool: Bool) throws -> SecAccess {
    var trustedApplications: [SecTrustedApplication] = []
    if trustCurrentTool {
        var trusted: SecTrustedApplication?
        let status = SecTrustedApplicationCreateFromPath(CommandLine.arguments[0], &trusted)
        guard status == errSecSuccess, let trusted else {
            throw fail("无法为本地工具创建 Keychain 信任项：\(securityMessage(status))")
        }
        trustedApplications.append(trusted)
    }

    let descriptor: CFString = label as NSString
    let trustedList: CFArray = trustedApplications as NSArray
    var access: SecAccess?
    let status = SecAccessCreate(descriptor, trustedList, &access)
    guard status == errSecSuccess, let access else {
        throw fail("无法创建 Keychain ACL：\(securityMessage(status))")
    }
    return access
}

private func updateKeychainData(service: String, account: String, data: Data) throws {
    let query = baseQuery(service: service, account: account)
    let status = SecItemUpdate(query as CFDictionary, [kSecValueData: data] as CFDictionary)
    guard status == errSecSuccess else {
        throw fail("更新 Keychain 失败：\(securityMessage(status))")
    }
}

private func deleteKeychainData(service: String, account: String) throws {
    let status = SecItemDelete(baseQuery(service: service, account: account) as CFDictionary)
    guard status == errSecSuccess || status == errSecItemNotFound else {
        throw fail("删除 Keychain 条目失败：\(securityMessage(status))")
    }
}

private func loadIndex() throws -> VaultIndex {
    guard let data = try readKeychainData(service: indexService, account: vaultAccount) else {
        return VaultIndex()
    }
    do {
        return try JSONDecoder().decode(VaultIndex.self, from: data)
    } catch {
        throw fail("密码库索引损坏，停止操作以免误删：\(error.localizedDescription)")
    }
}

private func saveIndex(_ index: VaultIndex) throws {
    let encoder = JSONEncoder()
    encoder.outputFormatting = [.sortedKeys]
    let data = try encoder.encode(index)
    if try readKeychainData(service: indexService, account: vaultAccount) == nil {
        try addKeychainData(
            service: indexService,
            account: vaultAccount,
            data: data,
            label: "Rain Studio 钥匙串密码库索引",
            requireConfirmation: false
        )
    } else {
        try updateKeychainData(service: indexService, account: vaultAccount, data: data)
    }
}

private func readHiddenLine(prompt: String) throws -> String {
    guard isatty(STDIN_FILENO) == 1 else {
        throw fail("当前标准输入不是终端；请使用 --secret-clipboard 或可信管道配合 --secret-stdin")
    }
    FileHandle.standardError.write(Data(prompt.utf8))

    var original = termios()
    guard tcgetattr(STDIN_FILENO, &original) == 0 else { throw fail("无法关闭终端回显") }
    var hidden = original
    hidden.c_lflag &= ~tcflag_t(ECHO)
    guard tcsetattr(STDIN_FILENO, TCSAFLUSH, &hidden) == 0 else { throw fail("无法关闭终端回显") }
    defer {
        var restored = original
        _ = tcsetattr(STDIN_FILENO, TCSAFLUSH, &restored)
        FileHandle.standardError.write(Data("\n".utf8))
    }

    guard let value = readLine(strippingNewline: true) else { throw fail("没有读到密码") }
    return value
}

private func trimSingleTrailingNewline(_ data: Data) -> Data {
    var bytes = Array(data)
    if bytes.last == 10 {
        bytes.removeLast()
        if bytes.last == 13 { bytes.removeLast() }
    }
    return Data(bytes)
}

private func readSecret(_ args: Arguments, confirmTwice: Bool) throws -> Data {
    let sources = [args.flags.contains("--secret-clipboard"), args.flags.contains("--secret-stdin")]
        .filter { $0 }.count
    guard sources <= 1 else { throw fail("密码来源只能选择一种") }

    let data: Data
    if args.flags.contains("--secret-clipboard") {
        guard let value = NSPasteboard.general.string(forType: .string), !value.isEmpty else {
            throw fail("系统剪贴板里没有可用文本")
        }
        data = Data(value.utf8)
        NSPasteboard.general.clearContents()
    } else if args.flags.contains("--secret-stdin") {
        data = trimSingleTrailingNewline(FileHandle.standardInput.readDataToEndOfFile())
    } else {
        let first = try readHiddenLine(prompt: "请输入密码：")
        if confirmTwice {
            let second = try readHiddenLine(prompt: "请再次输入密码：")
            guard first == second else { throw fail("两次输入的密码不一致") }
        }
        data = Data(first.utf8)
    }

    guard !data.isEmpty else { throw fail("密码不能为空") }
    guard data.count <= 32_768 else { throw fail("密码不能超过 32 KiB") }
    guard String(data: data, encoding: .utf8) != nil else { throw fail("密码必须是 UTF-8 文本") }
    return data
}

private func confirmed(_ prompt: String, token: String, args: Arguments) -> Bool {
    if args.flags.contains("--yes") { return true }
    FileHandle.standardError.write(Data("\(prompt) 输入 \(token) 继续：".utf8))
    return readLine()?.trimmingCharacters(in: .whitespacesAndNewlines) == token
}

private func requireOneName(_ args: Arguments) throws -> String {
    guard args.positionals.count == 1 else { throw fail("该命令需要且只需要一个条目名称") }
    return try checkedName(args.positionals[0])
}

private func printable(_ value: String?) -> String {
    guard let value, !value.isEmpty else { return "-" }
    return value.replacingOccurrences(of: "\n", with: " ")
}

private func printEntry(_ entry: Entry) {
    print("名称：\(entry.name)")
    print("账号：\(printable(entry.account))")
    print("网址：\(printable(entry.url))")
    print("备注：\(printable(entry.notes))")
    print("创建：\(entry.createdAt)")
    print("更新：\(entry.updatedAt)")
}

private func printJSON<T: Encodable>(_ value: T) throws {
    let encoder = JSONEncoder()
    encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
    let data = try encoder.encode(value)
    guard let output = String(data: data, encoding: .utf8) else { throw fail("无法生成 JSON") }
    print(output)
}

private func scheduleClipboardClear(changeCount: Int, seconds: Int) throws {
    let process = Process()
    let executable = CommandLine.arguments[0]
    if executable.hasSuffix(".swift") {
        process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
        process.arguments = ["swift", executable, "_clear-clipboard", String(changeCount), String(seconds)]
    } else {
        process.executableURL = URL(fileURLWithPath: executable)
        process.arguments = ["_clear-clipboard", String(changeCount), String(seconds)]
    }
    process.standardInput = FileHandle.nullDevice
    process.standardOutput = FileHandle.nullDevice
    process.standardError = FileHandle.nullDevice
    try process.run()
}

private func add(_ args: Arguments) throws {
    let name = try requireOneName(args)
    var index = try loadIndex()
    let entryKey = key(for: name)
    guard index.entries[entryKey] == nil else { throw fail("条目已存在；请使用 update，避免意外覆盖") }
    guard confirmed("将新增“\(name)”。", token: "ADD", args: args) else { throw fail("已取消新增") }

    let secret = try readSecret(args, confirmTwice: true)
    let now = isoTimestamp()
    let entry = Entry(
        name: name,
        account: args.values["--account"].flatMap { $0.isEmpty ? nil : $0 },
        url: args.values["--url"].flatMap { $0.isEmpty ? nil : $0 },
        notes: args.values["--notes"].flatMap { $0.isEmpty ? nil : $0 },
        createdAt: now,
        updatedAt: now
    )

    try addKeychainData(
        service: secretService,
        account: entryKey,
        data: secret,
        label: "Rain Studio 密码：\(name)",
        requireConfirmation: true
    )
    do {
        index.entries[entryKey] = entry
        try saveIndex(index)
    } catch {
        try? deleteKeychainData(service: secretService, account: entryKey)
        throw error
    }
    print("已新增：\(name)。密码未显示。")
}

private func list(_ args: Arguments) throws {
    guard args.positionals.isEmpty else { throw fail("list 不接受条目名称") }
    let entries = try loadIndex().entries.values.sorted {
        $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending
    }
    if args.flags.contains("--json") {
        try printJSON(entries)
        return
    }
    if entries.isEmpty {
        print("密码库为空。")
        return
    }
    for entry in entries {
        print("\(entry.name)\t账号=\(printable(entry.account))\t网址=\(printable(entry.url))\t更新=\(entry.updatedAt)")
    }
}

private func info(_ args: Arguments) throws {
    let name = try requireOneName(args)
    guard let entry = try loadIndex().entries[key(for: name)] else { throw fail("条目不存在：\(name)") }
    if args.flags.contains("--json") {
        try printJSON(entry)
    } else {
        printEntry(entry)
    }
}

private func get(_ args: Arguments) throws {
    let name = try requireOneName(args)
    let entryKey = key(for: name)
    guard let entry = try loadIndex().entries[entryKey] else { throw fail("条目不存在：\(name)") }
    if args.flags.contains("--show") {
        guard confirmed("明文会进入终端和工具输出。", token: "SHOW", args: args) else {
            throw fail("已取消明文显示")
        }
    }
    guard let data = try readKeychainData(service: secretService, account: entryKey) else {
        throw fail("索引存在，但密码条目缺失；请删除索引条目后重新新增")
    }
    guard let secret = String(data: data, encoding: .utf8) else { throw fail("密码不是 UTF-8 文本") }

    if args.flags.contains("--show") {
        print(secret)
        return
    }

    let seconds: Int
    if let raw = args.values["--clear-after"] {
        guard let parsed = Int(raw), (5...600).contains(parsed) else {
            throw fail("--clear-after 必须是 5 到 600 秒")
        }
        seconds = parsed
    } else {
        seconds = defaultClipboardSeconds
    }

    let pasteboard = NSPasteboard.general
    pasteboard.clearContents()
    guard pasteboard.setString(secret, forType: .string) else { throw fail("写入系统剪贴板失败") }
    let changeCount = pasteboard.changeCount
    try scheduleClipboardClear(changeCount: changeCount, seconds: seconds)
    print("已复制“\(entry.name)”的密码；未显示明文。剪贴板未被改写时将在 \(seconds) 秒后清除。")
}

private func update(_ args: Arguments) throws {
    let name = try requireOneName(args)
    var index = try loadIndex()
    let originalIndex = index
    let entryKey = key(for: name)
    guard var entry = index.entries[entryKey] else { throw fail("条目不存在：\(name)") }

    let metadataFlags = ["--account", "--url", "--notes"].filter { args.values[$0] != nil }
    let replacingSecret = args.flags.contains("--replace-secret")
    guard !metadataFlags.isEmpty || replacingSecret else {
        throw fail("没有更新内容；请提供 --account、--url、--notes 或 --replace-secret")
    }
    guard confirmed("将更新“\(entry.name)”。", token: "UPDATE", args: args) else { throw fail("已取消更新") }

    let replacementSecret: Data?
    if replacingSecret {
        replacementSecret = try readSecret(args, confirmTwice: true)
    } else if args.flags.contains("--secret-clipboard") || args.flags.contains("--secret-stdin") {
        throw fail("只有加 --replace-secret 时才能提供新密码")
    } else {
        replacementSecret = nil
    }

    if let value = args.values["--account"] { entry.account = value.isEmpty ? nil : value }
    if let value = args.values["--url"] { entry.url = value.isEmpty ? nil : value }
    if let value = args.values["--notes"] { entry.notes = value.isEmpty ? nil : value }
    entry.updatedAt = isoTimestamp()
    index.entries[entryKey] = entry
    try saveIndex(index)
    if let replacementSecret {
        do {
            try updateKeychainData(
                service: secretService,
                account: entryKey,
                data: replacementSecret
            )
        } catch {
            try? saveIndex(originalIndex)
            throw error
        }
    }
    print("已更新：\(entry.name)。密码未显示。")
}

private func delete(_ args: Arguments) throws {
    let name = try requireOneName(args)
    var index = try loadIndex()
    let entryKey = key(for: name)
    guard let entry = index.entries[entryKey] else { throw fail("条目不存在：\(name)") }
    guard confirmed("将永久删除“\(entry.name)”。", token: "DELETE", args: args) else { throw fail("已取消删除") }

    _ = try readKeychainData(service: secretService, account: entryKey)
    try deleteKeychainData(service: secretService, account: entryKey)
    index.entries.removeValue(forKey: entryKey)
    try saveIndex(index)
    print("已删除：\(entry.name)。")
}

private func selfTest() throws {
    let suffix = UUID().uuidString.lowercased()
    let service = "com.rainstudio.local-password-vault.self-test.\(suffix)"
    let account = "test"
    let original = Data(UUID().uuidString.utf8)
    let updated = Data(UUID().uuidString.utf8)
    defer { try? deleteKeychainData(service: service, account: account) }

    try addKeychainData(
        service: service,
        account: account,
        data: original,
        label: "Rain Studio 密码库自检",
        requireConfirmation: false
    )
    guard try readKeychainData(service: service, account: account) == original else {
        throw fail("自检读取结果不一致")
    }
    try updateKeychainData(service: service, account: account, data: updated)
    guard try readKeychainData(service: service, account: account) == updated else {
        throw fail("自检更新结果不一致")
    }
    try deleteKeychainData(service: service, account: account)
    guard try readKeychainData(service: service, account: account) == nil else {
        throw fail("自检条目删除失败")
    }

    let sample = VaultIndex(entries: [
        "test": Entry(
            name: "test",
            account: "user",
            url: "https://example.invalid",
            notes: nil,
            createdAt: isoTimestamp(),
            updatedAt: isoTimestamp()
        )
    ])
    let encoded = try JSONEncoder().encode(sample)
    _ = try JSONDecoder().decode(VaultIndex.self, from: encoded)
    print("自检通过：Keychain 新增、读取、更新、删除和索引编解码正常；测试条目已删除。")
}

private func doctor() throws {
#if os(macOS)
    print("平台：macOS")
    print("存储：登录 Keychain，本地条目不启用同步")
    print("密码访问：ACL 不预先信任应用；请在系统弹窗中选择“允许”，不要选择“始终允许”")
    print("默认调取：剪贴板，\(defaultClipboardSeconds) 秒后按 changeCount 条件清除")
    _ = try loadIndex()
    print("状态：可用")
#else
    throw fail("仅支持 macOS")
#endif
}

private func clearClipboardHelper(_ args: Arguments) throws {
    guard args.positionals.count == 2,
          let expectedChangeCount = Int(args.positionals[0]),
          let seconds = UInt32(args.positionals[1]) else {
        throw fail("内部剪贴板清理参数无效")
    }
    sleep(seconds)
    let pasteboard = NSPasteboard.general
    if pasteboard.changeCount == expectedChangeCount {
        pasteboard.clearContents()
    }
}

private func usage() {
    print("""
    钥匙串密码库

    用法：
      password_vault.swift doctor
      password_vault.swift self-test
      password_vault.swift add NAME [--account VALUE] [--url VALUE] [--notes VALUE]
                                [--secret-clipboard|--secret-stdin] [--yes]
      password_vault.swift list [--json]
      password_vault.swift info NAME [--json]
      password_vault.swift get NAME [--show] [--clear-after 5..600] [--yes]
      password_vault.swift update NAME [--account VALUE] [--url VALUE] [--notes VALUE]
                                   [--replace-secret] [--secret-clipboard|--secret-stdin] [--yes]
      password_vault.swift delete NAME [--yes]

    安全规则：不要把密码放进命令参数或环境变量。默认用无回显终端输入；
    在 Agent 场景中，优先由用户把密码放入剪贴板后使用 --secret-clipboard。
    """)
}

private func run() throws {
    let args = try parseArguments(Array(CommandLine.arguments.dropFirst()))
    switch args.command {
    case "help", "--help", "-h": usage()
    case "doctor": try doctor()
    case "self-test": try selfTest()
    case "add": try add(args)
    case "list": try list(args)
    case "info": try info(args)
    case "get": try get(args)
    case "update": try update(args)
    case "delete": try delete(args)
    case "_clear-clipboard": try clearClipboardHelper(args)
    default: throw fail("未知命令：\(args.command)。运行 help 查看用法")
    }
}

do {
    try run()
} catch let error as VaultFailure {
    FileHandle.standardError.write(Data("错误：\(error.description)\n".utf8))
    exit(1)
} catch {
    FileHandle.standardError.write(Data("错误：\(error.localizedDescription)\n".utf8))
    exit(1)
}
