import AVFoundation
import SwiftUI

/// The QR scanner  the real entry point to a ride.
///
/// AVFoundation's `AVCaptureMetadataOutput` recognises QR codes natively, so unlike
/// Android (CameraX + ML Kit) this needs no third-party dependency and no model download.
///
/// Manual entry stays reachable from every state  camera denied, camera missing, or a
/// sticker too scratched to read. Never trap the rider.
struct QrScannerView: View {
    let onResult: (String) -> Void
    let onManualEntry: () -> Void
    let onClose: () -> Void

    @State private var authorization = AVCaptureDevice.authorizationStatus(for: .video)
    @State private var found = false
    @State private var torchOn = false

    var body: some View {
        VStack(alignment: .leading, spacing: 14) {
            HStack {
                Text("scan_title".localized).font(.beebSection)
                Spacer()
                SecondaryButton(title: "scan_close".localized, action: onClose)
                    .frame(width: 120)
            }

            Text("scan_hint".localized)
                .font(.beebBody)
                .foregroundStyle(Color.muted)

            switch authorization {
            case .authorized:
                scanner
            case .notDetermined:
                permissionCard(denied: false)
                Spacer()
                BrandButton(title: "scan_grant".localized) {
                    AVCaptureDevice.requestAccess(for: .video) { granted in
                        Task { @MainActor in
                            authorization = granted ? .authorized : .denied
                        }
                    }
                }
            default:
                permissionCard(denied: true)
                Spacer()
            }

            SecondaryButton(title: "scan_manual".localized, action: onManualEntry)
        }
        .padding(20)
        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
        .background(Color.canvas)
    }

    private var scanner: some View {
        ZStack {
            CameraLayer(torchOn: torchOn) { raw in
                // The session stops itself on the first read; this guards the SwiftUI
                // side against a duplicate delivery re-entering the ride flow.
                guard !found else { return }
                found = true
                onResult(QrCode.normalise(raw))
            }
            .clipShape(RoundedRectangle(cornerRadius: BeebRadius.card))

            Reticle(found: found)

            VStack {
                HStack {
                    StatusBadge(
                        text: (found ? "scan_found" : "scan_searching").localized,
                        tone: found ? .success : .brand
                    )
                    Spacer()
                }
                Spacer()
                if TorchControl.isAvailable {
                    HStack {
                        Spacer()
                        SecondaryButton(
                            title: (torchOn ? "scan_torch_off" : "scan_torch_on").localized
                        ) {
                            torchOn.toggle()
                        }
                        .frame(width: 180)
                    }
                }
            }
            .padding(14)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }

    private func permissionCard(denied: Bool) -> some View {
        VStack(alignment: .leading, spacing: 4) {
            IconChip(glyph: "⛶", tone: denied ? .danger : .brand)
                .padding(.bottom, 8)
            Text("scan_permission_title".localized).font(.beebBody)
            Text("scan_permission_body".localized)
                .font(.beebCaption)
                .foregroundStyle(Color.muted)
        }
        .padding(18)
        .beebCard()
    }
}

/// Corner frame + a sweeping line, so it's obvious where to aim.
private struct Reticle: View {
    let found: Bool
    @State private var sweep: CGFloat = 0

    private var tone: Color { found ? .success : .brand }

    var body: some View {
        GeometryReader { geo in
            let side = geo.size.width * 0.72
            ZStack {
                RoundedRectangle(cornerRadius: BeebRadius.card)
                    .strokeBorder(tone, lineWidth: 2)
                    .frame(width: side, height: side)

                if !found {
                    Rectangle()
                        .fill(tone)
                        .frame(width: side, height: 2)
                        .offset(y: (sweep - 0.5) * side)
                }
            }
            .frame(width: geo.size.width, height: geo.size.height)
        }
        .allowsHitTesting(false)
        .onAppear {
            withAnimation(.easeInOut(duration: 1.8).repeatForever(autoreverses: true)) {
                sweep = 1
            }
        }
    }
}

/// Torch state is a device-level property, so it's toggled directly rather than
/// threaded through the capture session.
enum TorchControl {
    static var isAvailable: Bool {
        AVCaptureDevice.default(for: .video)?.hasTorch ?? false
    }

    static func set(_ on: Bool) {
        guard let device = AVCaptureDevice.default(for: .video), device.hasTorch else { return }
        try? device.lockForConfiguration()
        device.torchMode = on ? .on : .off
        device.unlockForConfiguration()
    }
}

/// Scooter QR stickers commonly encode a deep link rather than the bare code
/// (`https://beebbeeb.sa/s/SCT-0042`, or `...?code=SCT-0042`). Send booking-service the
/// code either way.
enum QrCode {
    static func normalise(_ raw: String) -> String {
        let value = raw.trimmingCharacters(in: .whitespacesAndNewlines)
        guard value.hasPrefix("http://") || value.hasPrefix("https://"),
              let components = URLComponents(string: value) else { return value }
        if let code = components.queryItems?.first(where: { $0.name == "code" || $0.name == "qr" })?.value {
            return code
        }
        let last = components.path.split(separator: "/").last.map(String.init)
        return last ?? value
    }
}

// MARK: - Capture session

/// AVCaptureSession bridged into SwiftUI. SwiftUI has no native capture view.
private struct CameraLayer: UIViewRepresentable {
    let torchOn: Bool
    let onFound: (String) -> Void

    func makeUIView(context: Context) -> PreviewView {
        let view = PreviewView()
        context.coordinator.start(on: view, onFound: onFound)
        return view
    }

    func updateUIView(_ view: PreviewView, context: Context) {
        TorchControl.set(torchOn)
    }

    static func dismantleUIView(_ view: PreviewView, coordinator: Coordinator) {
        // Leaving the torch on after the scanner closes is a real-world annoyance
        // (and a battery cost) riders would have to fix in Settings.
        TorchControl.set(false)
        coordinator.stop()
    }

    func makeCoordinator() -> Coordinator { Coordinator() }

    /// A UIView whose backing layer *is* the preview layer, so it resizes with Auto
    /// Layout instead of needing manual frame bookkeeping.
    final class PreviewView: UIView {
        override class var layerClass: AnyClass { AVCaptureVideoPreviewLayer.self }
        var previewLayer: AVCaptureVideoPreviewLayer { layer as! AVCaptureVideoPreviewLayer }
    }

    final class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate {
        private let session = AVCaptureSession()
        private var onFound: ((String) -> Void)?
        private var claimed = false

        func start(on view: PreviewView, onFound: @escaping (String) -> Void) {
            self.onFound = onFound

            guard let device = AVCaptureDevice.default(for: .video),
                  let input = try? AVCaptureDeviceInput(device: device),
                  session.canAddInput(input) else { return }
            session.addInput(input)

            let output = AVCaptureMetadataOutput()
            guard session.canAddOutput(output) else { return }
            session.addOutput(output)
            output.setMetadataObjectsDelegate(self, queue: .main)
            // Must be set *after* the output is attached, or the type isn't yet available.
            output.metadataObjectTypes = [.qr]

            view.previewLayer.session = session
            view.previewLayer.videoGravity = .resizeAspectFill

            // startRunning blocks; keeping it off the main thread avoids a hitch as the
            // scanner appears.
            Task.detached { [session] in session.startRunning() }
        }

        func stop() {
            guard session.isRunning else { return }
            Task.detached { [session] in session.stopRunning() }
        }

        func metadataOutput(
            _ output: AVCaptureMetadataOutput,
            didOutput metadataObjects: [AVMetadataObject],
            from connection: AVCaptureConnection
        ) {
            // One read per scanner presentation  a second allocation of the same
            // scooter would 409 against the rider's own ride.
            guard !claimed,
                  let object = metadataObjects.first as? AVMetadataMachineReadableCodeObject,
                  object.type == .qr,
                  let value = object.stringValue else { return }
            claimed = true
            stop()
            onFound?(value)
        }
    }
}
