import SwiftUI
import AVFoundation
import NucleicProtocol
import os

/// Filterable scanner diagnostics — in Console.app / Xcode, filter by subsystem
/// `xyz.blakeslee.nucleic.remote` (category `scanner`) or search the "📷" marker.
private let scannerLog = Logger(subsystem: "xyz.blakeslee.nucleic.remote", category: "scanner")

/// Why the live camera feed isn't showing, so the scanner never sits on a silent black screen.
enum ScannerCameraState: Equatable {
    case scanning          // feed is live
    case denied            // permission off — needs Settings
    case unavailable       // no camera on this device (e.g. the Simulator)
    case couldNotStart     // camera exists + authorized but the session won't run
                           // (iPhone locked / mirrored to a Mac / in use elsewhere)
}

/// Camera QR scanner (UX_IOS §7). Decodes the Mac's `nucleic://pair?d=…` QR into a
/// `PairingPayload` and hands it back exactly once.
struct PairingScannerView: View {
    let onScanned: (PairingPayload) -> Void
    @Environment(\.dismiss) private var dismiss
    @State private var error: String?
    @State private var cameraState: ScannerCameraState = .scanning
    /// Bumping this rebuilds the scanner controller from scratch (fresh session) — the "Try
    /// Again" path after the camera couldn't start.
    @State private var retryToken = 0

    var body: some View {
        NavigationStack {
            ZStack {
                Color.black.ignoresSafeArea()
                QRScannerRepresentable(onFound: handle, onState: { cameraState = $0 })
                    .id(retryToken)
                    .ignoresSafeArea()
                VStack {
                    Spacer()
                    if cameraState == .scanning {
                        RoundedRectangle(cornerRadius: 16)
                            .stroke(.white.opacity(0.8), lineWidth: 3)
                            .frame(width: 240, height: 240)
                    }
                    Spacer()
                    statusBanner
                    Spacer().frame(height: 40)
                }
                .padding(.horizontal, 24)
            }
            .navigationTitle("Scan to pair")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } }
        }
    }

    @ViewBuilder
    private var statusBanner: some View {
        if let error {
            banner(error, tint: .red.opacity(0.85))
        } else {
            switch cameraState {
            case .scanning:
                banner("Point at the QR code on your Mac", tint: .black.opacity(0.5))
            case .unavailable:
                VStack(spacing: 12) {
                    banner("The camera isn't available here — pairing needs a real iPhone, not the Simulator.",
                           tint: .black.opacity(0.6))
                }
            case .denied:
                VStack(spacing: 12) {
                    banner("Camera access is off. Enable it to scan the pairing code.", tint: .black.opacity(0.6))
                    Button("Open Settings") {
                        if let url = URL(string: UIApplication.openSettingsURLString) {
                            UIApplication.shared.open(url)
                        }
                    }
                    .buttonStyle(.borderedProminent)
                }
            case .couldNotStart:
                VStack(spacing: 12) {
                    banner("Couldn't start the camera. If your iPhone is locked or being mirrored to a Mac, the camera is blocked — open Nucleic directly on your unlocked iPhone, then try again.",
                           tint: .black.opacity(0.6))
                    Button("Try Again") { cameraState = .scanning; retryToken += 1 }
                        .buttonStyle(.borderedProminent)
                }
            }
        }
    }

    private func banner(_ text: String, tint: Color) -> some View {
        Text(text)
            .multilineTextAlignment(.center)
            .foregroundStyle(.white)
            .padding(.horizontal, 16).padding(.vertical, 10)
            .background(tint, in: RoundedRectangle(cornerRadius: 16))
    }

    private func handle(_ value: String) {
        guard let payload = try? PairingPayload(qrString: value) else {
            error = "That isn't a Nucleic pairing code"; return
        }
        onScanned(payload)
    }
}

/// UIKit bridge for the AVCaptureSession metadata scan.
struct QRScannerRepresentable: UIViewControllerRepresentable {
    let onFound: (String) -> Void
    let onState: (ScannerCameraState) -> Void

    func makeUIViewController(context: Context) -> QRScannerController {
        let controller = QRScannerController()
        controller.onFound = onFound
        controller.onState = onState
        return controller
    }
    func updateUIViewController(_ controller: QRScannerController, context: Context) {}
}

/// A view whose *backing layer* is the camera preview, so it always fills the view through the
/// layer hierarchy — no manual frame bookkeeping. The prior version added the preview as a
/// sublayer and set its frame in viewDidLoad/viewDidLayoutSubviews, which raced with the async
/// permission callback and layout and could leave a zero-sized (black) preview on device.
final class CameraPreviewView: UIView {
    override class var layerClass: AnyClass { AVCaptureVideoPreviewLayer.self }
    var previewLayer: AVCaptureVideoPreviewLayer { layer as! AVCaptureVideoPreviewLayer }
}

final class QRScannerController: UIViewController, AVCaptureMetadataOutputObjectsDelegate {
    var onFound: ((String) -> Void)?
    var onState: ((ScannerCameraState) -> Void)?
    private let session = AVCaptureSession()
    /// AVCaptureSession isn't thread-safe — all configuration + start/stop happen here; only the
    /// preview layer and delegate callbacks touch the main thread.
    private let sessionQueue = DispatchQueue(label: "nucleic.qrscanner.session")
    private var configured = false
    private var startAttempts = 0
    private var didFind = false

    private var previewView: CameraPreviewView { view as! CameraPreviewView }

    override func loadView() { view = CameraPreviewView() }

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .black
        previewView.previewLayer.session = session
        previewView.previewLayer.videoGravity = .resizeAspectFill

        let nc = NotificationCenter.default
        nc.addObserver(self, selector: #selector(sessionDidStart),
                       name: .AVCaptureSessionDidStartRunning, object: session)
        nc.addObserver(self, selector: #selector(sessionRuntimeError(_:)),
                       name: .AVCaptureSessionRuntimeError, object: session)
        nc.addObserver(self, selector: #selector(sessionInterrupted(_:)),
                       name: .AVCaptureSessionWasInterrupted, object: session)
        // Recover the session once it's allowed to run again (interruption cleared, or the app
        // returned to the foreground) — the reason-1 interruption at presentation time left it
        // stopped with no retry.
        nc.addObserver(self, selector: #selector(startSessionIfReady),
                       name: .AVCaptureSessionInterruptionEnded, object: session)
        nc.addObserver(self, selector: #selector(startSessionIfReady),
                       name: UIApplication.didBecomeActiveNotification, object: nil)

        // Gate on camera authorization first — without this the prompt may never appear and the
        // feed silently fails (and on a denied device it would just stay black).
        let status = AVCaptureDevice.authorizationStatus(for: .video)
        scannerLog.notice("📷 authorization status=\(status.rawValue, privacy: .public)")
        switch status {
        case .authorized:
            configureSession()
        case .notDetermined:
            AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
                scannerLog.notice("📷 requestAccess granted=\(granted, privacy: .public)")
                granted ? self?.configureSession() : self?.report(.denied)
            }
        case .denied, .restricted:
            report(.denied)
        @unknown default:
            report(.denied)
        }
    }

    /// Configure + start the capture graph on the session queue. Reports `.unavailable` when
    /// there's no usable camera (e.g. the Simulator) and `.scanning` once frames are flowing.
    private func configureSession() {
        sessionQueue.async { [weak self] in
            guard let self else { return }
            self.session.beginConfiguration()
            if self.session.canSetSessionPreset(.high) { self.session.sessionPreset = .high }
            guard let device = AVCaptureDevice.default(for: .video) else {
                self.session.commitConfiguration()
                scannerLog.error("📷 no video device — reporting unavailable")
                self.report(.unavailable); return
            }
            scannerLog.notice("📷 device=\(device.localizedName, privacy: .public)")
            guard let input = try? AVCaptureDeviceInput(device: device), self.session.canAddInput(input) else {
                self.session.commitConfiguration()
                scannerLog.error("📷 cannot create/add camera input")
                self.report(.unavailable); return
            }
            self.session.addInput(input)

            let output = AVCaptureMetadataOutput()
            guard self.session.canAddOutput(output) else {
                self.session.commitConfiguration()
                scannerLog.error("📷 cannot add metadata output")
                self.report(.unavailable); return
            }
            self.session.addOutput(output)
            output.setMetadataObjectsDelegate(self, queue: .main)
            self.session.commitConfiguration()
            // availableMetadataObjectTypes is only populated after the output is connected.
            output.metadataObjectTypes = output.availableMetadataObjectTypes.contains(.qr)
                ? [.qr] : output.availableMetadataObjectTypes

            self.configured = true
            scannerLog.notice("📷 configured inputs=\(self.session.inputs.count, privacy: .public) outputs=\(self.session.outputs.count, privacy: .public)")
            self.startSessionIfReady()
        }
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        startSessionIfReady()
        // The presentation transition can leave the session transiently "backgrounded" even
        // though the app is foreground; a short delayed retry catches the settled state if the
        // immediate start was interrupted.
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) { [weak self] in self?.startSessionIfReady() }
    }

    /// Start the camera only once it can actually run — configured, on screen, and
    /// foreground-active. Starting during the sheet's presentation made iOS interrupt the
    /// session (`videoDeviceNotAvailableInBackground`, reason 1) and leave it stopped → black.
    /// Every start trigger (config finished, view appeared, delayed retry, interruption ended,
    /// app foregrounded) funnels here; the session-queue hop no-ops if already running.
    @objc private func startSessionIfReady() {
        DispatchQueue.main.async { [weak self] in
            guard let self, self.isViewLoaded else { return }
            let appState = UIApplication.shared.applicationState
            let sceneState = self.view.window?.windowScene?.activationState
            scannerLog.notice("📷 tryStart window=\(self.view.window != nil, privacy: .public) app=\(appState.rawValue, privacy: .public) scene=\(sceneState?.rawValue ?? -1, privacy: .public)")
            guard self.view.window != nil, appState == .active else { return }
            self.sessionQueue.async {
                guard self.configured, !self.session.isRunning else { return }
                self.session.startRunning()
                scannerLog.notice("📷 startRunning -> isRunning=\(self.session.isRunning, privacy: .public)")
            }
        }
    }

    @objc private func sessionDidStart() {
        DispatchQueue.main.async { [weak self] in
            guard let self else { return }
            self.startAttempts = 0
            self.report(.scanning)
            let connection = self.previewView.previewLayer.connection
            scannerLog.notice("📷 didStartRunning: previewConnection=\(connection != nil, privacy: .public) active=\(connection?.isActive ?? false, privacy: .public) viewBounds=\(self.view.bounds.debugDescription, privacy: .public)")
        }
    }

    @objc private func sessionRuntimeError(_ note: Notification) {
        let error = note.userInfo?[AVCaptureSessionErrorKey] as? NSError
        scannerLog.error("📷 runtimeError=\(error?.description ?? "nil", privacy: .public)")
        startSessionIfReady()  // e.g. media-services reset — try to bring it back
    }

    @objc private func sessionInterrupted(_ note: Notification) {
        let reason = (note.userInfo?[AVCaptureSessionInterruptionReasonKey] as? Int) ?? -1
        scannerLog.error("📷 sessionInterrupted reason=\(reason, privacy: .public)")
        // Reason 1 fires transiently while the sheet's presentation settles even though the app
        // is foreground; back off briefly and retry until it sticks (bounded).
        DispatchQueue.main.async { [weak self] in
            guard let self, self.startAttempts < 12 else {
                scannerLog.error("📷 gave up starting after retries")
                self?.report(.couldNotStart)
                return
            }
            self.startAttempts += 1
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in self?.startSessionIfReady() }
        }
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        sessionQueue.async { [session] in if session.isRunning { session.stopRunning() } }
    }

    private func report(_ state: ScannerCameraState) {
        DispatchQueue.main.async { [weak self] in self?.onState?(state) }
    }

    func metadataOutput(
        _ output: AVCaptureMetadataOutput,
        didOutput metadataObjects: [AVMetadataObject],
        from connection: AVCaptureConnection
    ) {
        guard !didFind,
              let object = metadataObjects.first as? AVMetadataMachineReadableCodeObject,
              let value = object.stringValue else { return }
        didFind = true
        onFound?(value)
    }
}
