import Foundation

/// On-screen placement for the agent's persistent preview window, baked into the render
/// entry. Applied only when the agent creates the window, so a user's later drag or resize
/// survives leaf edits. Nil means the window stays borderless and off-screen (tests,
/// headless daemons).
public struct JITRenderWindow: Sendable {
    public let x: Double
    public let y: Double
    public let width: Double
    public let height: Double
    public let title: String

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

/// Generates Swift source code that combines the original source file with a `@_cdecl` bridge
/// entry point, allowing the preview view to be loaded via `dlopen` + `dlsym`.
public enum BridgeGenerator {

    /// Generate combined source with DesignTimeStore + literal thunks + bridge entry point.
    ///
    /// - Parameters:
    ///   - originalSource: The full content of the original Swift source file.
    ///   - closureBody: The body of the `#Preview { ... }` closure from the original source.
    ///     Used as fallback if re-parsing the transformed source yields fewer previews than expected.
    ///   - previewIndex: 0-based index of which `#Preview` block to render (default: 0).
    ///   - entryPoint: The name of the C-callable entry function (default: `"createPreviewView"`).
    /// - Returns: Tuple of (source code ready to compile, literal entries for diffing).
    public static func generateCombinedSource(
        originalSource: String,
        closureBody: String,
        previewIndex: Int = 0,
        entryPoint: String = "createPreviewView",
        platform: PreviewPlatform = .macOS,
        traits: PreviewTraits = PreviewTraits(),
        setupModule: String? = nil,
        setupType: String? = nil,
        renderOutputPath: String? = nil,
        designTimeValuesPath: String? = nil,
        stableModuleImport: String? = nil,
        renderWindow: JITRenderWindow? = nil
    ) -> (source: String, literals: [LiteralEntry]) {
        // Transform source to replace literals with DesignTimeStore lookups
        let thunkResult = ThunkGenerator.transform(source: originalSource)

        // Re-parse transformed source to get the closure body with DesignTimeStore calls
        let previews = PreviewParser.parse(source: thunkResult.source)
        let transformedClosureBody: String
        if previewIndex < previews.count {
            transformedClosureBody = previews[previewIndex].closureBody
        } else {
            transformedClosureBody = closureBody
        }

        let modifiers = traitModifiers(traits)
        let hasSetup = isUsableSetup(module: setupModule, type: setupType)
        let setupImport = hasSetup ? "import \(setupModule!)\n" : ""
        let setUpEntry = hasSetup ? setUpEntryPoint(setupType: setupType!) : ""
        let viewCode =
            hasSetup
            ? viewWithSetup(closureBody: transformedClosureBody, setupType: setupType!, modifiers: modifiers)
            : """
            {
                        return SwiftUI.AnyView(__PreviewBridge.wrap {
                            \(transformedClosureBody)
                        }\(modifiers))
                    }()
            """

        let bodyKindEntry = bodyKindEntryPoint(closureBody: transformedClosureBody)
        let renderEntry =
            renderOutputPath.map {
                renderToFileEntryPoint(
                    viewCode: viewCode, path: $0, valuesPath: designTimeValuesPath,
                    window: renderWindow)
            } ?? ""
        let bridgeCode: String
        switch platform {
        case .macOS:
            bridgeCode = """
                import AppKit
                \(setupImport)
                \(setUpEntry)
                @_cdecl("\(entryPoint)")
                public func \(entryPoint)() -> UnsafeMutableRawPointer {
                    let view = \(viewCode)
                    let hostingView = NSHostingView(rootView: view)
                    return Unmanaged.passRetained(hostingView).toOpaque()
                }

                \(bodyKindEntry)

                \(renderEntry)
                """
        case .iOS:
            bridgeCode = """
                import UIKit
                \(setupImport)
                \(setUpEntry)
                @_cdecl("\(entryPoint)")
                public func \(entryPoint)() -> UnsafeMutableRawPointer {
                    let view = \(viewCode)
                    let hostingController = UIHostingController(rootView: view)
                    return Unmanaged.passRetained(hostingController).toOpaque()
                }

                \(bodyKindEntry)
                """
        }

        let stableImport =
            stableModuleImport.map { "@testable import \($0)\n" } ?? ""
        let combined = """
            \(stableImport)// --- DesignTimeStore ---
            \(DesignTimeStoreSource.code)

            // --- Preview bridge helper (generated by PreviewsMCP) ---
            \(PreviewBridgeSource.code(for: platform))

            // --- Source (with literal thunks) ---
            \(thunkResult.source)

            // --- Preview bridge (generated by PreviewsMCP) ---
            \(bridgeCode)
            """

        return (source: combined, literals: thunkResult.literals)
    }

    // MARK: - Project Mode: Tier 1 (bridge-only)

    /// Generate bridge-only source for project mode (Tier 1).
    /// Imports the target module and creates the @_cdecl entry point.
    /// No DesignTimeStore or original source — no literal hot-reload.
    public static func generateBridgeOnlySource(
        moduleName: String,
        closureBody: String,
        entryPoint: String = "createPreviewView",
        platform: PreviewPlatform = .macOS,
        traits: PreviewTraits = PreviewTraits(),
        setupModule: String? = nil,
        setupType: String? = nil
    ) -> String {
        let frameworkImport: String
        let hostCode: String

        switch platform {
        case .macOS:
            frameworkImport = "import AppKit"
            hostCode = """
                    let hostingView = NSHostingView(rootView: view)
                    return Unmanaged.passRetained(hostingView).toOpaque()
                """
        case .iOS:
            frameworkImport = "import UIKit"
            hostCode = """
                    let hostingController = UIHostingController(rootView: view)
                    return Unmanaged.passRetained(hostingController).toOpaque()
                """
        }

        let modifiers = traitModifiers(traits)
        let hasSetup = isUsableSetup(module: setupModule, type: setupType)
        let setupImport = hasSetup ? "\nimport \(setupModule!)" : ""
        let setUpEntry = hasSetup ? "\n\n\(setUpEntryPoint(setupType: setupType!))\n" : ""
        let viewCode =
            hasSetup
            ? viewWithSetup(closureBody: closureBody, setupType: setupType!, modifiers: modifiers)
            : """
            {
                    return SwiftUI.AnyView(__PreviewBridge.wrap {
                        \(closureBody)
                    }\(modifiers))
                }()
            """

        let bodyKindEntry = bodyKindEntryPoint(closureBody: closureBody)

        return """
            import SwiftUI
            \(frameworkImport)
            @testable import \(moduleName)\(setupImport)

            // --- Preview bridge helper (generated by PreviewsMCP) ---
            \(PreviewBridgeSource.code(for: platform))
            \(setUpEntry)
            @_cdecl("\(entryPoint)")
            public func \(entryPoint)() -> UnsafeMutableRawPointer {
                let view = \(viewCode)
                \(hostCode)
            }

            \(bodyKindEntry)
            """
    }

    // MARK: - Project Mode: Tier 2 (overlay with .o linking)

    /// Generate overlay source for project mode (Tier 2).
    /// Includes the preview file with ThunkGenerator transforms + DesignTimeStore + bridge.
    /// The preview file is compiled as part of the target module (same -module-name),
    /// while other target files are linked as pre-built .o files.
    public static func generateOverlaySource(
        originalSource: String,
        closureBody: String,
        previewIndex: Int = 0,
        entryPoint: String = "createPreviewView",
        platform: PreviewPlatform = .macOS,
        traits: PreviewTraits = PreviewTraits(),
        setupModule: String? = nil,
        setupType: String? = nil
    ) -> (source: String, literals: [LiteralEntry]) {
        return generateCombinedSource(
            originalSource: originalSource,
            closureBody: closureBody,
            previewIndex: previewIndex,
            entryPoint: entryPoint,
            platform: platform,
            traits: traits,
            setupModule: setupModule,
            setupType: setupType
        )
    }

    // MARK: - __PreviewBridge.wrap
    //
    // The generated bridge passes the user's closure body to
    // `__PreviewBridge.wrap { <body> }` (declared in `PreviewBridgeSource`),
    // which dispatches through Swift overload resolution:
    //
    //   - SwiftUI bodies (any `V: SwiftUI.View`) match a `@ViewBuilder` generic
    //     overload — preserving every construct Xcode's `#Preview` accepts:
    //     `if #available`, leading `let`/`var` declarations, branches with
    //     different concrete types, etc. The `@ViewBuilder` attribute lifts
    //     statement-only restrictions via `_ConditionalContent`.
    //   - UIKit bodies (`UIView`, `UIViewController`, and their subclasses)
    //     match dedicated non-generic overloads that wrap the view in a
    //     `UIViewRepresentable` / `UIViewControllerRepresentable` bridge —
    //     matching what Xcode's `#Preview` macro does for UIKit bodies.
    //
    // This replaces an earlier design that declared a nested
    // `@ViewBuilder func __previewBody() -> some SwiftUI.View { <body> }`,
    // which rejected UIKit bodies with
    // "'ExampleUIView' does not conform to 'View'".
    //
    // Caveat: UIKit has no `@ViewBuilder` equivalent, so multi-statement
    // UIKit bodies require an explicit `return`. Same constraint applies to
    // Xcode's first-party `#Preview` macro for UIKit.

    /// Validate that a string is safe to interpolate as a Swift identifier or dotted module path.
    private static func isValidSwiftIdentifier(_ s: String) -> Bool {
        let pattern = #"^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$"#
        return s.range(of: pattern, options: .regularExpression) != nil
    }

    /// Whether a setup module/type pair will actually produce setup code (the wrap and the
    /// `previewSetUp` entry). Callers that advertise setup downstream (e.g. a build's
    /// setupEntrySymbol) must use the same predicate, or they promise an entry the generated
    /// source does not contain.
    public static func isUsableSetup(module: String?, type: String?) -> Bool {
        guard let module, let type else { return false }
        return isValidSwiftIdentifier(module) && isValidSwiftIdentifier(type)
    }

    /// Escape a runtime string (path, window title) for interpolation into a generated Swift
    /// string literal. Backslash and quote would change the literal's structure; control
    /// characters (a newline in a file name is legal on macOS) would split it across lines.
    static func escapedForSwiftStringLiteral(_ s: String) -> String {
        var out = ""
        for scalar in s.unicodeScalars {
            switch scalar {
            case "\\": out += "\\\\"
            case "\"": out += "\\\""
            case let c where c.properties.generalCategory == .control:
                out += "\\u{\(String(c.value, radix: 16))}"
            default: out.unicodeScalars.append(scalar)
            }
        }
        return out
    }

    /// Generate the `@_cdecl("previewBodyKind")` entry point. The compiler picks the same
    /// `__PreviewBodyKindProbe.detect` overload that `__PreviewBridge.wrap` does, so the
    /// returned value reflects the outermost body kind: 1 = SwiftUI, 2 = UIView,
    /// 3 = UIViewController. Used by the daemon to skip the literal-only fast path for
    /// UIKit bodies (#160).
    private static func bodyKindEntryPoint(closureBody: String) -> String {
        """
        @_cdecl("previewBodyKind")
        public func previewBodyKind() -> Int32 {
            return __PreviewBodyKindProbe.detect {
                \(closureBody)
            }
        }
        """
    }

    /// Generate the `@_cdecl("renderPreviewToFile")` entry point (macOS, model-A JIT path).
    /// Builds the same preview view as `createPreviewView`, hosts it in a borderless
    /// `NSWindow` positioned off-screen via `NSHostingView` (AppKit-backed views like
    /// `List` need a real window's backing hierarchy — `ImageRenderer` returns nil or a
    /// blank raster for them), captures at a deterministic 1x like `Snapshot.capture`,
    /// and writes a PNG to `path`. Nullary so it runs over the agent's `runOnMain`
    /// surface; the path is baked in (the daemon recompiles per structural edit).
    ///
    /// The window is process-persistent: each generation's entry looks it up by
    /// identifier in the agent's window list and swaps its content view, so the agent
    /// keeps one live window across structural edits (single-renderer consolidation).
    /// AppKit state is shared across JITDylib generations; the entry's own globals are not.
    private static func renderToFileEntryPoint(
        viewCode: String, path: String, valuesPath: String?, window: JITRenderWindow?
    ) -> String {
        let seed =
            valuesPath.map {
                """
                if let __dtData = try? Data(contentsOf: URL(fileURLWithPath: "\(escapedForSwiftStringLiteral($0))")),
                    let __dtValues = try? JSONSerialization.jsonObject(with: __dtData)
                        as? [String: Any]
                {
                    DesignTimeStore.shared.values = __dtValues
                }
                """
            } ?? ""
        let createWindow: String
        let presentNewWindow: String
        if let window {
            let title = escapedForSwiftStringLiteral(window.title)
            createWindow = """
                NSApplication.shared.setActivationPolicy(.accessory)
                                let created = NSWindow(
                                    contentRect: NSRect(
                                        x: \(window.x), y: \(window.y),
                                        width: \(window.width), height: \(window.height)),
                                    styleMask: [.titled, .closable, .miniaturizable, .resizable],
                                    backing: .buffered, defer: false)
                                created.title = "\(title)"
                """
            // A visible window takes key status and activates the agent once, at
            // creation, matching what the daemon's dylib window start used to do.
            // Re-renders use orderFrontRegardless so edits never steal focus.
            presentNewWindow = """
                window.makeKeyAndOrderFront(nil)
                                NSApplication.shared.activate(ignoringOtherApps: true)
                """
        } else {
            createWindow = """
                let created = NSWindow(
                                    contentRect: NSRect(x: 0, y: 0, width: 400, height: 600),
                                    styleMask: [.borderless], backing: .buffered, defer: false)
                                created.setFrameOrigin(NSPoint(x: -10_000, y: -10_000))
                """
            presentNewWindow = "window.orderFrontRegardless()"
        }
        return """
            @_cdecl("renderPreviewToFile")
            public func renderPreviewToFile() -> Int32 {
                MainActor.assumeIsolated {
                    \(seed)
                    let view = \(viewCode)
                    let identifier = NSUserInterfaceItemIdentifier("previewsmcp-preview")
                    var isNewWindow = false
                    let window =
                        NSApplication.shared.windows.first { $0.identifier == identifier }
                        ?? {
                            \(createWindow)
                            created.identifier = identifier
                            created.isReleasedWhenClosed = false
                            isNewWindow = true
                            return created
                        }()
                    let hosting = NSHostingView(rootView: view)
                    hosting.sizingOptions = []
                    window.contentView = hosting
                    if isNewWindow {
                        \(presentNewWindow)
                    } else {
                        window.orderFrontRegardless()
                    }
                    hosting.layoutSubtreeIfNeeded()
                    let bounds = hosting.bounds
                    guard bounds.width > 0, bounds.height > 0,
                        let rep = NSBitmapImageRep(
                            bitmapDataPlanes: nil,
                            pixelsWide: Int(bounds.width.rounded()),
                            pixelsHigh: Int(bounds.height.rounded()),
                            bitsPerSample: 8,
                            samplesPerPixel: 4,
                            hasAlpha: true,
                            isPlanar: false,
                            colorSpaceName: .deviceRGB,
                            bytesPerRow: 0,
                            bitsPerPixel: 0
                        )
                    else { return Int32(-1) }
                    rep.size = bounds.size
                    hosting.cacheDisplay(in: bounds, to: rep)
                    guard let data = rep.representation(using: .png, properties: [:]) else {
                        return Int32(-2)
                    }
                    do {
                        try data.write(to: URL(fileURLWithPath: "\(escapedForSwiftStringLiteral(path))"))
                    } catch {
                        return Int32(-3)
                    }
                    return 0
                }
            }
            """
    }

    /// Generate the `@_cdecl("previewSetUp")` entry point that bridges async setUp.
    private static func setUpEntryPoint(setupType: String) -> String {
        """
        @_cdecl("previewSetUp")
        public func previewSetUp() {
            let semaphore = DispatchSemaphore(value: 0)
            Task {
                try? await \(setupType).setUp()
                semaphore.signal()
            }
            semaphore.wait()
        }
        """
    }

    /// Generate view code with setup wrapping. The closure body is threaded through
    /// `__PreviewBridge.wrap` (which dispatches to SwiftUI / UIKit overloads via overload
    /// resolution) before being handed to the setup plugin's `wrap`. Traits are applied
    /// OUTSIDE the setup wrap so explicit overrides take precedence.
    private static func viewWithSetup(closureBody: String, setupType: String, modifiers: String) -> String {
        """
        {
                let innerView = __PreviewBridge.wrap {
                    \(closureBody)
                }
                let wrappedView = \(setupType).wrap(innerView)
                return SwiftUI.AnyView(
                    wrappedView\(modifiers)
                )
            }()
        """
    }

    /// Build SwiftUI modifier chain for the given traits.
    private static func traitModifiers(_ traits: PreviewTraits) -> String {
        var mods = ""
        if let cs = traits.colorScheme {
            mods += "\n            .preferredColorScheme(.\(cs))"
        }
        if let dts = traits.dynamicTypeSize {
            mods += "\n            .dynamicTypeSize(.\(dts))"
        }
        if let locale = traits.locale {
            mods += "\n            .environment(\\.locale, Locale(identifier: \"\(locale)\"))"
        }
        if let ld = traits.layoutDirection {
            mods += "\n            .environment(\\.layoutDirection, .\(ld))"
        }
        if let lw = traits.legibilityWeight {
            mods += "\n            .environment(\\.legibilityWeight, .\(lw))"
        }
        return mods
    }
}
