import Foundation

/// Single source of truth mapping each DSClient call to HTTP method + path + query + body.
/// Trailing slashes are significant (e.g. `documents/`, `search/`) and are preserved by building the
/// URL from the base string rather than appendingPathComponent (which strips them).
enum DSEndpoint {
    case login(String, String, String?)
    case me
    case refresh
    case logout
    case documents(DocumentListQuery)
    case document(Int)
    case documentContent(Int)
    case documentTree
    case categoryCounts
    case duplicates
    case patchDocument(Int, DocumentUpdate)
    case putContent(Int, String)
    case deleteDocument(Int)
    case search(String, SearchMode?, Int?, Bool?)
    case ask(String, Int?, String?, Bool?)
    case memos(MemoListQuery)
    case memo(Int)
    case createMemo(MemoCreate)
    case patchMemo(Int, MemoUpdate)
    case pinMemo(Int, Bool)
    case archiveMemo(Int, Bool)
    case toggleMemoTask(Int, Int, Bool)
    case deleteMemo(Int)
    case digest(String?, String?)

    var method: String {
        switch self {
        case .login, .refresh, .logout, .createMemo: return "POST"
        case .patchDocument, .patchMemo, .pinMemo, .archiveMemo, .toggleMemoTask: return "PATCH"
        case .putContent: return "PUT"
        case .deleteDocument, .deleteMemo: return "DELETE"
        default: return "GET"
        }
    }

    var path: String {
        switch self {
        case .login: return "auth/login"
        case .me: return "auth/me"
        case .refresh: return "auth/refresh"
        case .logout: return "auth/logout"
        case .documents: return "documents/"
        case .document(let id): return "documents/\(id)"
        case .documentContent(let id): return "documents/\(id)/content"
        case .documentTree: return "documents/tree"
        case .categoryCounts: return "documents/stats/category-counts"
        case .duplicates: return "documents/duplicates"
        case .patchDocument(let id, _): return "documents/\(id)"
        case .putContent(let id, _): return "documents/\(id)/content"
        case .deleteDocument(let id): return "documents/\(id)"
        case .search: return "search/"
        case .ask: return "search/ask"
        case .memos: return "memos/"
        case .memo(let id): return "memos/\(id)"
        case .createMemo: return "memos/"
        case .patchMemo(let id, _): return "memos/\(id)"
        case .pinMemo(let id, _): return "memos/\(id)/pin"
        case .archiveMemo(let id, _): return "memos/\(id)/archive"
        case .toggleMemoTask(let id, let idx, _): return "memos/\(id)/tasks/\(idx)"
        case .deleteMemo(let id): return "memos/\(id)"
        case .digest: return "digest"
        }
    }

    /// Bearer header applies to everything except login/refresh (refresh rides the HttpOnly cookie).
    var requiresBearer: Bool {
        switch self {
        case .login, .refresh: return false
        default: return true
        }
    }

    var queryItems: [URLQueryItem] {
        var items: [URLQueryItem] = []
        func add(_ name: String, _ value: String?) { if let value { items.append(URLQueryItem(name: name, value: value)) } }
        switch self {
        case .documents(let q):
            add("page", String(q.page)); add("page_size", String(q.pageSize))
            add("domain", q.domain); add("sub_group", q.subGroup); add("source", q.source)
            add("format", q.format); add("review_status", q.reviewStatus); add("category", q.category)
        case .search(let qq, let mode, let page, let debug):
            add("q", qq); add("mode", mode?.rawValue); add("page", page.map(String.init)); add("debug", debug.map(String.init))
        case .ask(let qq, let limit, let backend, let debug):
            add("q", qq); add("limit", limit.map(String.init)); add("backend", backend); add("debug", debug.map(String.init))
        case .memos(let q):
            add("page", String(q.page)); add("page_size", String(q.pageSize))
            add("pinned", q.pinned.map(String.init)); add("archived", q.archived.map(String.init))
        case .digest(let date, let country):
            add("date", date); add("country", country)
        default:
            break
        }
        return items
    }

    func httpBody(_ encoder: JSONEncoder) throws -> Data? {
        switch self {
        case .login(let u, let p, let totp):
            return try encoder.encode(LoginBody(username: u, password: p, totpCode: totp))
        case .patchDocument(_, let update):
            return try encoder.encode(update)
        case .putContent(_, let content):
            return try encoder.encode(ContentBody(content: content))
        case .createMemo(let create):
            return try encoder.encode(create)
        case .patchMemo(_, let update):
            return try encoder.encode(update)
        case .pinMemo(_, let pinned):
            return try encoder.encode(PinnedBody(pinned: pinned))
        case .archiveMemo(_, let archived):
            return try encoder.encode(ArchivedBody(archived: archived))
        case .toggleMemoTask(_, _, let checked):
            return try encoder.encode(CheckedBody(checked: checked))
        default:
            return nil
        }
    }
}

private struct LoginBody: Encodable {
    let username: String
    let password: String
    let totpCode: String?
    enum CodingKeys: String, CodingKey { case username, password; case totpCode = "totp_code" }
}
private struct ContentBody: Encodable { let content: String }
private struct PinnedBody: Encodable { let pinned: Bool }
private struct ArchivedBody: Encodable { let archived: Bool }
private struct CheckedBody: Encodable { let checked: Bool }
