import Foundation

/// Client → Host messages (SYNC_PROTOCOL §5.1). The phone subscribes, approves, and — at
/// `control` scope — drives the same actions the Mac can (start chats, manage to-dos, manage
/// and integrate sessions).
public enum ClientMsg: Sendable, Equatable {
    case hello(Hello)
    case listSessions
    case listDashboard                        // request Home/Projects/To-Dos state
    case subscribe(Subscribe)
    case unsubscribe(SessionID)
    case approvalRespond(ApprovalID, Decision)
    case sendInput(SessionID, AgentInput)     // scope ≥ approve
    case interrupt(SessionID)                 // scope ≥ control
    case ping
    // Control scope:
    case startChat(StartChatRequest)
    case captureTodo(CaptureTodoRequest)
    case dispatchTodo(TodoID, ProjectID)
    case setTodoStatus(TodoID, TodoStatus)
    case deleteTodo(TodoID)
    case renameSession(SessionID, String)
    case setFavorite(SessionID, Bool)
    case setArchived(SessionID, Bool)
    case deleteSession(SessionID)
    case integrate(SessionID, IntegrationMode)
    case discard(SessionID)
    // Control scope: change a session's model / reasoning / automation mid-stream (the same
    // affordances the Mac header exposes). `nil` model/effort resets to the host/app default.
    case setSessionModel(SessionID, String?)
    case setSessionEffort(SessionID, String?)
    case setSessionAuto(SessionID, Bool)
    case setSessionAutoShip(SessionID, Bool)
    case setSessionShipBranch(SessionID, String?)
}

/// Host → Client messages (SYNC_PROTOCOL §5.2). Forward-compatible: unknown tags decode to
/// `.unknown` rather than throwing, so a newer host can add cases without breaking older
/// clients (SYNC §9).
public enum HostMsg: Sendable, Equatable {
    case welcome(Welcome)
    case sessionList([SessionSummary])
    case snapshot(SessionSnapshot)
    case events(EventBatch)
    case approvalRequested(ApprovalRequest)
    case approvalResolved(ApprovalResolved)
    case sessionUpdated(SessionSummary)
    case dashboard(DashboardSnapshot)
    case error(WireError)
    case pong
    /// An unrecognized future message tag (forward-compat); the client ignores it.
    case unknown(String)
}

// MARK: - Codable (tagged: {"t": <tag>, ...payload})

private enum EnvelopeKey: String, CodingKey {
    case t, sessionID, approvalID, decision, input, tag
    case request, todoID, projectID, todoStatus, title, flag, mode
    case model, effort, branch
}

extension ClientMsg: Codable {
    public init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: EnvelopeKey.self)
        switch try c.decode(String.self, forKey: .t) {
        case "hello": self = .hello(try Hello(from: decoder))
        case "listSessions": self = .listSessions
        case "subscribe": self = .subscribe(try Subscribe(from: decoder))
        case "unsubscribe": self = .unsubscribe(try c.decode(SessionID.self, forKey: .sessionID))
        case "approvalRespond":
            self = .approvalRespond(
                try c.decode(ApprovalID.self, forKey: .approvalID),
                try c.decode(Decision.self, forKey: .decision))
        case "sendInput":
            self = .sendInput(
                try c.decode(SessionID.self, forKey: .sessionID),
                try c.decode(AgentInput.self, forKey: .input))
        case "interrupt": self = .interrupt(try c.decode(SessionID.self, forKey: .sessionID))
        case "ping": self = .ping
        case "listDashboard": self = .listDashboard
        case "startChat": self = .startChat(try c.decode(StartChatRequest.self, forKey: .request))
        case "captureTodo": self = .captureTodo(try c.decode(CaptureTodoRequest.self, forKey: .request))
        case "dispatchTodo":
            self = .dispatchTodo(
                try c.decode(TodoID.self, forKey: .todoID),
                try c.decode(ProjectID.self, forKey: .projectID))
        case "setTodoStatus":
            self = .setTodoStatus(
                try c.decode(TodoID.self, forKey: .todoID),
                try c.decode(TodoStatus.self, forKey: .todoStatus))
        case "deleteTodo": self = .deleteTodo(try c.decode(TodoID.self, forKey: .todoID))
        case "renameSession":
            self = .renameSession(
                try c.decode(SessionID.self, forKey: .sessionID),
                try c.decode(String.self, forKey: .title))
        case "setFavorite":
            self = .setFavorite(
                try c.decode(SessionID.self, forKey: .sessionID),
                try c.decode(Bool.self, forKey: .flag))
        case "setArchived":
            self = .setArchived(
                try c.decode(SessionID.self, forKey: .sessionID),
                try c.decode(Bool.self, forKey: .flag))
        case "deleteSession": self = .deleteSession(try c.decode(SessionID.self, forKey: .sessionID))
        case "integrate":
            self = .integrate(
                try c.decode(SessionID.self, forKey: .sessionID),
                try c.decode(IntegrationMode.self, forKey: .mode))
        case "discard": self = .discard(try c.decode(SessionID.self, forKey: .sessionID))
        case "setSessionModel":
            self = .setSessionModel(
                try c.decode(SessionID.self, forKey: .sessionID),
                try c.decodeIfPresent(String.self, forKey: .model))
        case "setSessionEffort":
            self = .setSessionEffort(
                try c.decode(SessionID.self, forKey: .sessionID),
                try c.decodeIfPresent(String.self, forKey: .effort))
        case "setSessionAuto":
            self = .setSessionAuto(
                try c.decode(SessionID.self, forKey: .sessionID),
                try c.decode(Bool.self, forKey: .flag))
        case "setSessionAutoShip":
            self = .setSessionAutoShip(
                try c.decode(SessionID.self, forKey: .sessionID),
                try c.decode(Bool.self, forKey: .flag))
        case "setSessionShipBranch":
            self = .setSessionShipBranch(
                try c.decode(SessionID.self, forKey: .sessionID),
                try c.decodeIfPresent(String.self, forKey: .branch))
        case let other:
            throw DecodingError.dataCorruptedError(
                forKey: .t, in: c, debugDescription: "Unknown ClientMsg \(other)")
        }
    }

    public func encode(to encoder: Encoder) throws {
        var c = encoder.container(keyedBy: EnvelopeKey.self)
        switch self {
        case .hello(let h):
            try c.encode("hello", forKey: .t); try h.encode(to: encoder)
        case .listSessions:
            try c.encode("listSessions", forKey: .t)
        case .subscribe(let s):
            try c.encode("subscribe", forKey: .t); try s.encode(to: encoder)
        case .unsubscribe(let id):
            try c.encode("unsubscribe", forKey: .t); try c.encode(id, forKey: .sessionID)
        case .approvalRespond(let id, let decision):
            try c.encode("approvalRespond", forKey: .t)
            try c.encode(id, forKey: .approvalID)
            try c.encode(decision, forKey: .decision)
        case .sendInput(let id, let input):
            try c.encode("sendInput", forKey: .t)
            try c.encode(id, forKey: .sessionID)
            try c.encode(input, forKey: .input)
        case .interrupt(let id):
            try c.encode("interrupt", forKey: .t); try c.encode(id, forKey: .sessionID)
        case .ping:
            try c.encode("ping", forKey: .t)
        case .listDashboard:
            try c.encode("listDashboard", forKey: .t)
        case .startChat(let r):
            try c.encode("startChat", forKey: .t); try c.encode(r, forKey: .request)
        case .captureTodo(let r):
            try c.encode("captureTodo", forKey: .t); try c.encode(r, forKey: .request)
        case .dispatchTodo(let todo, let project):
            try c.encode("dispatchTodo", forKey: .t)
            try c.encode(todo, forKey: .todoID)
            try c.encode(project, forKey: .projectID)
        case .setTodoStatus(let todo, let status):
            try c.encode("setTodoStatus", forKey: .t)
            try c.encode(todo, forKey: .todoID)
            try c.encode(status, forKey: .todoStatus)
        case .deleteTodo(let todo):
            try c.encode("deleteTodo", forKey: .t); try c.encode(todo, forKey: .todoID)
        case .renameSession(let id, let title):
            try c.encode("renameSession", forKey: .t)
            try c.encode(id, forKey: .sessionID)
            try c.encode(title, forKey: .title)
        case .setFavorite(let id, let flag):
            try c.encode("setFavorite", forKey: .t)
            try c.encode(id, forKey: .sessionID)
            try c.encode(flag, forKey: .flag)
        case .setArchived(let id, let flag):
            try c.encode("setArchived", forKey: .t)
            try c.encode(id, forKey: .sessionID)
            try c.encode(flag, forKey: .flag)
        case .deleteSession(let id):
            try c.encode("deleteSession", forKey: .t); try c.encode(id, forKey: .sessionID)
        case .integrate(let id, let mode):
            try c.encode("integrate", forKey: .t)
            try c.encode(id, forKey: .sessionID)
            try c.encode(mode, forKey: .mode)
        case .discard(let id):
            try c.encode("discard", forKey: .t); try c.encode(id, forKey: .sessionID)
        case .setSessionModel(let id, let model):
            try c.encode("setSessionModel", forKey: .t)
            try c.encode(id, forKey: .sessionID)
            try c.encodeIfPresent(model, forKey: .model)
        case .setSessionEffort(let id, let effort):
            try c.encode("setSessionEffort", forKey: .t)
            try c.encode(id, forKey: .sessionID)
            try c.encodeIfPresent(effort, forKey: .effort)
        case .setSessionAuto(let id, let flag):
            try c.encode("setSessionAuto", forKey: .t)
            try c.encode(id, forKey: .sessionID)
            try c.encode(flag, forKey: .flag)
        case .setSessionAutoShip(let id, let flag):
            try c.encode("setSessionAutoShip", forKey: .t)
            try c.encode(id, forKey: .sessionID)
            try c.encode(flag, forKey: .flag)
        case .setSessionShipBranch(let id, let branch):
            try c.encode("setSessionShipBranch", forKey: .t)
            try c.encode(id, forKey: .sessionID)
            try c.encodeIfPresent(branch, forKey: .branch)
        }
    }
}

extension HostMsg: Codable {
    public init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: EnvelopeKey.self)
        let tag = try c.decode(String.self, forKey: .t)
        switch tag {
        case "welcome": self = .welcome(try Welcome(from: decoder))
        case "sessionList": self = .sessionList(try c.decode([SessionSummary].self, forKey: .input))
        case "snapshot": self = .snapshot(try SessionSnapshot(from: decoder))
        case "events": self = .events(try EventBatch(from: decoder))
        case "approvalRequested": self = .approvalRequested(try ApprovalRequest(from: decoder))
        case "approvalResolved": self = .approvalResolved(try ApprovalResolved(from: decoder))
        case "sessionUpdated": self = .sessionUpdated(try SessionSummary(from: decoder))
        case "dashboard": self = .dashboard(try DashboardSnapshot(from: decoder))
        case "error": self = .error(try WireError(from: decoder))
        case "pong": self = .pong
        default: self = .unknown(tag)  // forward-compatible
        }
    }

    public func encode(to encoder: Encoder) throws {
        var c = encoder.container(keyedBy: EnvelopeKey.self)
        switch self {
        case .welcome(let w):
            try c.encode("welcome", forKey: .t); try w.encode(to: encoder)
        case .sessionList(let list):
            try c.encode("sessionList", forKey: .t); try c.encode(list, forKey: .input)
        case .snapshot(let s):
            try c.encode("snapshot", forKey: .t); try s.encode(to: encoder)
        case .events(let b):
            try c.encode("events", forKey: .t); try b.encode(to: encoder)
        case .approvalRequested(let r):
            try c.encode("approvalRequested", forKey: .t); try r.encode(to: encoder)
        case .approvalResolved(let r):
            try c.encode("approvalResolved", forKey: .t); try r.encode(to: encoder)
        case .sessionUpdated(let s):
            try c.encode("sessionUpdated", forKey: .t); try s.encode(to: encoder)
        case .dashboard(let d):
            try c.encode("dashboard", forKey: .t); try d.encode(to: encoder)
        case .error(let e):
            try c.encode("error", forKey: .t); try e.encode(to: encoder)
        case .pong:
            try c.encode("pong", forKey: .t)
        case .unknown(let tag):
            try c.encode(tag, forKey: .t)
        }
    }
}
