import Foundation

/// Narrow protocol over the two secrets cairn needs: the Immich server
/// URL and the API key. The iOS target backs this with Keychain (so a
/// device wipe or app delete removes credentials); the CLI reads from
/// process environment, typically populated from a `.env` file via
/// `EnvFileLoader`.
///
/// Both accessors throw `SecretStoreError.missing` on absence rather
/// than returning optionals — callers treat missing credentials as a
/// hard failure, and a thrown error carries the variable name for the
/// user-facing message.
public protocol SecretStore: Sendable {
    func serverURL() throws -> URL
    func apiKey() throws -> String

    /// The Immich user UUID that the API key authenticates as. Cached
    /// at setup-time (after the first successful `verifyServer`) and
    /// used as the per-user discriminator in cairn's partition key
    /// (alongside server URL). `nil` if not yet cached — callers should
    /// degrade gracefully (defer per-user state load, show "—" stats)
    /// rather than fail when this returns nil.
    func userId() throws -> String?

    /// The Immich user email associated with the API key. Cached
    /// alongside `userId` at setup time; used as a human-readable label
    /// when surfacing account info in UI (sign-out screens, multi-
    /// account disambiguation) and for filesystem debugging.
    /// Defensive: not the partition discriminator — `userId` is the
    /// stable Immich primitive (survives email changes).
    func userEmail() throws -> String?
}

public enum SecretStoreError: Error, CustomStringConvertible, Equatable {
    case missing(name: String)
    case invalidURL(value: String)

    public var description: String {
        switch self {
        case .missing(let name):
            return "missing required secret: \(name)"
        case .invalidURL(let value):
            return "not a valid URL: \(value)"
        }
    }
}

/// `SecretStore` backed by `ProcessInfo.processInfo.environment`. The
/// CLI is the primary consumer; an iOS app would use a Keychain-backed
/// implementation instead. Populate the environment before
/// constructing — typically via `EnvFileLoader.load(fromPath:)` at
/// process start.
public struct EnvSecretStore: SecretStore {
    public let urlVariable: String
    public let keyVariable: String

    public init(urlVariable: String = "IMMICH_URL", keyVariable: String = "IMMICH_API_KEY") {
        self.urlVariable = urlVariable
        self.keyVariable = keyVariable
    }

    public func serverURL() throws -> URL {
        let raw = ProcessInfo.processInfo.environment[urlVariable] ?? ""
        guard !raw.isEmpty else { throw SecretStoreError.missing(name: urlVariable) }
        guard let url = URL(string: raw) else { throw SecretStoreError.invalidURL(value: raw) }
        return url
    }

    public func apiKey() throws -> String {
        let raw = ProcessInfo.processInfo.environment[keyVariable] ?? ""
        guard !raw.isEmpty else { throw SecretStoreError.missing(name: keyVariable) }
        return raw
    }

    /// CLI doesn't partition by user — the CLI is single-user-per-process
    /// and per-server state lives in process working dir. Always returns
    /// nil so callers fall back to URL-only partitioning, matching
    /// pre-userId-partitioning CLI behavior.
    public func userId() throws -> String? { nil }
    public func userEmail() throws -> String? { nil }
}

/// Parses `KEY=VALUE` pairs from a `.env` file into process environment.
/// Existing environment variables take precedence; quoted values have
/// surrounding quotes stripped; lines beginning with `#` are ignored.
/// No-op if the file doesn't exist.
public enum EnvFileLoader {
    public static func load(fromPath path: String) {
        guard let contents = try? String(contentsOfFile: path, encoding: .utf8) else { return }
        for line in contents.split(whereSeparator: \.isNewline) {
            let trimmed = line.trimmingCharacters(in: .whitespaces)
            guard !trimmed.isEmpty, !trimmed.hasPrefix("#") else { continue }
            guard let eq = trimmed.firstIndex(of: "=") else { continue }
            let key = String(trimmed[..<eq]).trimmingCharacters(in: .whitespaces)
            var value = String(trimmed[trimmed.index(after: eq)...]).trimmingCharacters(in: .whitespaces)
            if value.count >= 2 {
                let first = value.first!, last = value.last!
                if (first == "\"" && last == "\"") || (first == "'" && last == "'") {
                    value = String(value.dropFirst().dropLast())
                }
            }
            if ProcessInfo.processInfo.environment[key] == nil {
                setenv(key, value, 1)
            }
        }
    }
}
