import CoreGraphics
import Foundation

// MARK: - Capabilities

public enum Capability: String, Codable, CaseIterable, Sendable {
    /// AppleScript / Automation access to control other apps (TCC Automation).
    case appleScript
    case notifications
    case accessibility
    case screenRecording
    case microphone
    case speechRecognition
    case camera
    case location
}

public enum CameraFacing: String, Codable, Sendable {
    case front
    case back
}

// MARK: - Requests

/// Notification interruption level (maps to UNNotificationInterruptionLevel)
public enum NotificationPriority: String, Codable, Sendable {
    case passive // silent, no wake
    case active // default
    case timeSensitive // breaks through Focus modes
}

/// Notification delivery mechanism.
public enum NotificationDelivery: String, Codable, Sendable {
    /// Use macOS notification center (UNUserNotificationCenter).
    case system
    /// Use an in-app overlay/toast (no Notification Center history).
    case overlay
    /// Prefer system; fall back to overlay when system isn't available.
    case auto
}

// MARK: - Canvas geometry

/// Optional placement hints for the Canvas panel.
/// Values are in screen coordinates (same as `NSWindow` frame).
public struct CanvasPlacement: Codable, Sendable {
    public var x: Double?
    public var y: Double?
    public var width: Double?
    public var height: Double?

    public init(x: Double? = nil, y: Double? = nil, width: Double? = nil, height: Double? = nil) {
        self.x = x
        self.y = y
        self.width = width
        self.height = height
    }
}

// MARK: - Canvas show result

public enum CanvasShowStatus: String, Codable, Sendable {
    /// Panel was shown, but no navigation occurred (no target passed and session already existed).
    case shown
    /// Target was a direct URL (http(s) or file).
    case web
    /// Local canvas target resolved to an existing file.
    case ok
    /// Local canvas target did not resolve to a file (404 page).
    case notFound
    /// Local scaffold fallback (e.g., no index.html present).
    case welcome
}

public struct CanvasShowResult: Codable, Sendable {
    /// Session directory on disk (e.g. `~/Library/Application Support/Clawdbot/canvas/<session>/`).
    public var directory: String
    /// Target as provided by the caller (may be nil/empty).
    public var target: String?
    /// Target actually navigated to (nil when no navigation occurred; defaults to "/" for a newly created session).
    public var effectiveTarget: String?
    public var status: CanvasShowStatus
    /// URL that was loaded (nil when no navigation occurred).
    public var url: String?

    public init(
        directory: String,
        target: String?,
        effectiveTarget: String?,
        status: CanvasShowStatus,
        url: String?)
    {
        self.directory = directory
        self.target = target
        self.effectiveTarget = effectiveTarget
        self.status = status
        self.url = url
    }
}

// MARK: - Canvas A2UI

public enum CanvasA2UICommand: String, Codable, Sendable {
    case pushJSONL
    case reset
}

public enum Request: Sendable {
    case notify(
        title: String,
        body: String,
        sound: String?,
        priority: NotificationPriority?,
        delivery: NotificationDelivery?)
    case ensurePermissions([Capability], interactive: Bool)
    case runShell(
        command: [String],
        cwd: String?,
        env: [String: String]?,
        timeoutSec: Double?,
        needsScreenRecording: Bool)
    case status
    case agent(message: String, thinking: String?, session: String?, deliver: Bool, to: String?)
    case rpcStatus
    case canvasPresent(session: String, path: String?, placement: CanvasPlacement?)
    case canvasHide(session: String)
    case canvasEval(session: String, javaScript: String)
    case canvasSnapshot(session: String, outPath: String?)
    case canvasA2UI(session: String, command: CanvasA2UICommand, jsonl: String?)
    case nodeList
    case nodeDescribe(nodeId: String)
    case nodeInvoke(nodeId: String, command: String, paramsJSON: String?)
    case cameraSnap(facing: CameraFacing?, maxWidth: Int?, quality: Double?, outPath: String?)
    case cameraClip(facing: CameraFacing?, durationMs: Int?, includeAudio: Bool, outPath: String?)
    case screenRecord(screenIndex: Int?, durationMs: Int?, fps: Double?, includeAudio: Bool, outPath: String?)
}

// MARK: - Responses

public struct Response: Codable, Sendable {
    public var ok: Bool
    public var message: String?
    /// Optional payload (PNG bytes, stdout text, etc.).
    public var payload: Data?

    public init(ok: Bool, message: String? = nil, payload: Data? = nil) {
        self.ok = ok
        self.message = message
        self.payload = payload
    }
}

// MARK: - Codable conformance for Request

extension Request: Codable {
    private enum CodingKeys: String, CodingKey {
        case type
        case title, body, sound, priority, delivery
        case caps, interactive
        case command, cwd, env, timeoutSec, needsScreenRecording
        case message, thinking, session, deliver, to
        case rpcStatus
        case path
        case javaScript
        case outPath
        case screenIndex
        case fps
        case canvasA2UICommand
        case jsonl
        case facing
        case maxWidth
        case quality
        case durationMs
        case includeAudio
        case placement
        case nodeId
        case nodeCommand
        case paramsJSON
    }

    private enum Kind: String, Codable {
        case notify
        case ensurePermissions
        case runShell
        case status
        case agent
        case rpcStatus
        case canvasPresent
        case canvasHide
        case canvasEval
        case canvasSnapshot
        case canvasA2UI
        case nodeList
        case nodeDescribe
        case nodeInvoke
        case cameraSnap
        case cameraClip
        case screenRecord
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        switch self {
        case let .notify(title, body, sound, priority, delivery):
            try container.encode(Kind.notify, forKey: .type)
            try container.encode(title, forKey: .title)
            try container.encode(body, forKey: .body)
            try container.encodeIfPresent(sound, forKey: .sound)
            try container.encodeIfPresent(priority, forKey: .priority)
            try container.encodeIfPresent(delivery, forKey: .delivery)

        case let .ensurePermissions(caps, interactive):
            try container.encode(Kind.ensurePermissions, forKey: .type)
            try container.encode(caps, forKey: .caps)
            try container.encode(interactive, forKey: .interactive)

        case let .runShell(command, cwd, env, timeoutSec, needsSR):
            try container.encode(Kind.runShell, forKey: .type)
            try container.encode(command, forKey: .command)
            try container.encodeIfPresent(cwd, forKey: .cwd)
            try container.encodeIfPresent(env, forKey: .env)
            try container.encodeIfPresent(timeoutSec, forKey: .timeoutSec)
            try container.encode(needsSR, forKey: .needsScreenRecording)

        case .status:
            try container.encode(Kind.status, forKey: .type)

        case let .agent(message, thinking, session, deliver, to):
            try container.encode(Kind.agent, forKey: .type)
            try container.encode(message, forKey: .message)
            try container.encodeIfPresent(thinking, forKey: .thinking)
            try container.encodeIfPresent(session, forKey: .session)
            try container.encode(deliver, forKey: .deliver)
            try container.encodeIfPresent(to, forKey: .to)

        case .rpcStatus:
            try container.encode(Kind.rpcStatus, forKey: .type)

        case let .canvasPresent(session, path, placement):
            try container.encode(Kind.canvasPresent, forKey: .type)
            try container.encode(session, forKey: .session)
            try container.encodeIfPresent(path, forKey: .path)
            try container.encodeIfPresent(placement, forKey: .placement)

        case let .canvasHide(session):
            try container.encode(Kind.canvasHide, forKey: .type)
            try container.encode(session, forKey: .session)

        case let .canvasEval(session, javaScript):
            try container.encode(Kind.canvasEval, forKey: .type)
            try container.encode(session, forKey: .session)
            try container.encode(javaScript, forKey: .javaScript)

        case let .canvasSnapshot(session, outPath):
            try container.encode(Kind.canvasSnapshot, forKey: .type)
            try container.encode(session, forKey: .session)
            try container.encodeIfPresent(outPath, forKey: .outPath)

        case let .canvasA2UI(session, command, jsonl):
            try container.encode(Kind.canvasA2UI, forKey: .type)
            try container.encode(session, forKey: .session)
            try container.encode(command, forKey: .canvasA2UICommand)
            try container.encodeIfPresent(jsonl, forKey: .jsonl)

        case .nodeList:
            try container.encode(Kind.nodeList, forKey: .type)

        case let .nodeDescribe(nodeId):
            try container.encode(Kind.nodeDescribe, forKey: .type)
            try container.encode(nodeId, forKey: .nodeId)

        case let .nodeInvoke(nodeId, command, paramsJSON):
            try container.encode(Kind.nodeInvoke, forKey: .type)
            try container.encode(nodeId, forKey: .nodeId)
            try container.encode(command, forKey: .nodeCommand)
            try container.encodeIfPresent(paramsJSON, forKey: .paramsJSON)

        case let .cameraSnap(facing, maxWidth, quality, outPath):
            try container.encode(Kind.cameraSnap, forKey: .type)
            try container.encodeIfPresent(facing, forKey: .facing)
            try container.encodeIfPresent(maxWidth, forKey: .maxWidth)
            try container.encodeIfPresent(quality, forKey: .quality)
            try container.encodeIfPresent(outPath, forKey: .outPath)

        case let .cameraClip(facing, durationMs, includeAudio, outPath):
            try container.encode(Kind.cameraClip, forKey: .type)
            try container.encodeIfPresent(facing, forKey: .facing)
            try container.encodeIfPresent(durationMs, forKey: .durationMs)
            try container.encode(includeAudio, forKey: .includeAudio)
            try container.encodeIfPresent(outPath, forKey: .outPath)

        case let .screenRecord(screenIndex, durationMs, fps, includeAudio, outPath):
            try container.encode(Kind.screenRecord, forKey: .type)
            try container.encodeIfPresent(screenIndex, forKey: .screenIndex)
            try container.encodeIfPresent(durationMs, forKey: .durationMs)
            try container.encodeIfPresent(fps, forKey: .fps)
            try container.encode(includeAudio, forKey: .includeAudio)
            try container.encodeIfPresent(outPath, forKey: .outPath)
        }
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let kind = try container.decode(Kind.self, forKey: .type)
        switch kind {
        case .notify:
            let title = try container.decode(String.self, forKey: .title)
            let body = try container.decode(String.self, forKey: .body)
            let sound = try container.decodeIfPresent(String.self, forKey: .sound)
            let priority = try container.decodeIfPresent(NotificationPriority.self, forKey: .priority)
            let delivery = try container.decodeIfPresent(NotificationDelivery.self, forKey: .delivery)
            self = .notify(title: title, body: body, sound: sound, priority: priority, delivery: delivery)

        case .ensurePermissions:
            let caps = try container.decode([Capability].self, forKey: .caps)
            let interactive = try container.decode(Bool.self, forKey: .interactive)
            self = .ensurePermissions(caps, interactive: interactive)

        case .runShell:
            let command = try container.decode([String].self, forKey: .command)
            let cwd = try container.decodeIfPresent(String.self, forKey: .cwd)
            let env = try container.decodeIfPresent([String: String].self, forKey: .env)
            let timeout = try container.decodeIfPresent(Double.self, forKey: .timeoutSec)
            let needsSR = try container.decode(Bool.self, forKey: .needsScreenRecording)
            self = .runShell(command: command, cwd: cwd, env: env, timeoutSec: timeout, needsScreenRecording: needsSR)

        case .status:
            self = .status

        case .agent:
            let message = try container.decode(String.self, forKey: .message)
            let thinking = try container.decodeIfPresent(String.self, forKey: .thinking)
            let session = try container.decodeIfPresent(String.self, forKey: .session)
            let deliver = try container.decode(Bool.self, forKey: .deliver)
            let to = try container.decodeIfPresent(String.self, forKey: .to)
            self = .agent(message: message, thinking: thinking, session: session, deliver: deliver, to: to)

        case .rpcStatus:
            self = .rpcStatus

        case .canvasPresent:
            let session = try container.decode(String.self, forKey: .session)
            let path = try container.decodeIfPresent(String.self, forKey: .path)
            let placement = try container.decodeIfPresent(CanvasPlacement.self, forKey: .placement)
            self = .canvasPresent(session: session, path: path, placement: placement)

        case .canvasHide:
            let session = try container.decode(String.self, forKey: .session)
            self = .canvasHide(session: session)

        case .canvasEval:
            let session = try container.decode(String.self, forKey: .session)
            let javaScript = try container.decode(String.self, forKey: .javaScript)
            self = .canvasEval(session: session, javaScript: javaScript)

        case .canvasSnapshot:
            let session = try container.decode(String.self, forKey: .session)
            let outPath = try container.decodeIfPresent(String.self, forKey: .outPath)
            self = .canvasSnapshot(session: session, outPath: outPath)

        case .canvasA2UI:
            let session = try container.decode(String.self, forKey: .session)
            let command = try container.decode(CanvasA2UICommand.self, forKey: .canvasA2UICommand)
            let jsonl = try container.decodeIfPresent(String.self, forKey: .jsonl)
            self = .canvasA2UI(session: session, command: command, jsonl: jsonl)

        case .nodeList:
            self = .nodeList

        case .nodeDescribe:
            let nodeId = try container.decode(String.self, forKey: .nodeId)
            self = .nodeDescribe(nodeId: nodeId)

        case .nodeInvoke:
            let nodeId = try container.decode(String.self, forKey: .nodeId)
            let command = try container.decode(String.self, forKey: .nodeCommand)
            let paramsJSON = try container.decodeIfPresent(String.self, forKey: .paramsJSON)
            self = .nodeInvoke(nodeId: nodeId, command: command, paramsJSON: paramsJSON)

        case .cameraSnap:
            let facing = try container.decodeIfPresent(CameraFacing.self, forKey: .facing)
            let maxWidth = try container.decodeIfPresent(Int.self, forKey: .maxWidth)
            let quality = try container.decodeIfPresent(Double.self, forKey: .quality)
            let outPath = try container.decodeIfPresent(String.self, forKey: .outPath)
            self = .cameraSnap(facing: facing, maxWidth: maxWidth, quality: quality, outPath: outPath)

        case .cameraClip:
            let facing = try container.decodeIfPresent(CameraFacing.self, forKey: .facing)
            let durationMs = try container.decodeIfPresent(Int.self, forKey: .durationMs)
            let includeAudio = (try? container.decode(Bool.self, forKey: .includeAudio)) ?? true
            let outPath = try container.decodeIfPresent(String.self, forKey: .outPath)
            self = .cameraClip(facing: facing, durationMs: durationMs, includeAudio: includeAudio, outPath: outPath)

        case .screenRecord:
            let screenIndex = try container.decodeIfPresent(Int.self, forKey: .screenIndex)
            let durationMs = try container.decodeIfPresent(Int.self, forKey: .durationMs)
            let fps = try container.decodeIfPresent(Double.self, forKey: .fps)
            let includeAudio = (try? container.decode(Bool.self, forKey: .includeAudio)) ?? true
            let outPath = try container.decodeIfPresent(String.self, forKey: .outPath)
            self = .screenRecord(
                screenIndex: screenIndex,
                durationMs: durationMs,
                fps: fps,
                includeAudio: includeAudio,
                outPath: outPath)
        }
    }
}

// Shared transport settings
public let controlSocketPath =
    FileManager.default
        .homeDirectoryForCurrentUser
        .appendingPathComponent("Library/Application Support/clawdbot/control.sock")
        .path
