import SwiftUI
import UIKit
import GravitySDK

@available(iOS 15.0, *)
struct ServerDrivenRecommendationWidget: View {
    let selector: String
    let pageContext: PageContext
    let onOpenUrl: (URL) -> Void
    let onOpenDeeplink: (String) -> Void
    let onOpenProduct: ([String: Any?]) -> Void

    @State private var campaign: Campaign?
    @State private var content: CampaignContent?
    @State private var isLoading = false
    @State private var hasSentContentImpression = false
    @State private var hasSentContentVisibleImpression = false
    @State private var visibleProductKeys = Set<String>()

    var body: some View {
        Group {
            if isLoading {
                ProgressView()
            } else if let campaign, let content {
                VisibilityObserver {
                    sendContentVisibleImpressionIfNeeded(content: content, campaign: campaign)
                } content: {
                    widgetBody(content: content, campaign: campaign)
                }
                .task(id: content.contentId) {
                    sendContentImpressionIfNeeded(content: content, campaign: campaign)
                }
            } else {
                EmptyView()
            }
        }
        .task {
            await loadContent()
        }
    }

    @ViewBuilder
    private func widgetBody(content: CampaignContent, campaign: Campaign) -> some View {
        let container = content.variables?.frameUI?.container
        let containerStyle = container?.style ?? .empty
        let alignment = horizontalAlignment(from: containerStyle.contentAlignment)

        VStack(alignment: alignment, spacing: 0) {
            ForEach(Array((content.variables?.elements ?? []).enumerated()), id: \.offset) { _, element in
                renderElement(
                    element,
                    content: content,
                    campaign: campaign
                )
            }
        }
        .padding(edgeInsets(from: containerStyle.padding))
        .frame(
            width: containerStyle.size?.width,
            height: containerStyle.size?.height,
            alignment: .topLeading
        )
        .frame(maxWidth: .infinity, alignment: .leading)
        .background(containerStyle.backgroundColor ?? Color.clear)
        .clipShape(
            RoundedRectangle(cornerRadius: containerStyle.cornerRadius ?? 0)
        )
        .padding(edgeInsets(from: containerStyle.margin))
    }

    @ViewBuilder
    private func renderElement(
        _ element: Element,
        content: CampaignContent,
        campaign: Campaign
    ) -> some View {
        switch element.type {
        case .text:
            Text(element.text ?? "")
                .font(.system(
                    size: element.style.fontSize ?? 16,
                    weight: fontWeight(from: element.style.fontWeight)
                ))
                .foregroundColor(element.style.textColor)
                .multilineTextAlignment(
                    element.style.contentAlignment?.toTextAlignment() ?? .leading
                )
                .frame(maxWidth: .infinity, alignment: alignment(from: element.style.contentAlignment))
                .padding(edgeInsets(from: element.style.margin))
                .onTapGesture {
                    handleOnClick(element.onClick)
                }

        case .image:
            if let src = element.src {
                RemoteImageView(
                    urlString: src,
                    height: element.style.size?.height ?? 120
                )
                .frame(maxWidth: .infinity)
                .padding(edgeInsets(from: element.style.margin))
                .onTapGesture {
                    handleOnClick(element.onClick)
                }
            }

        case .button:
            Button {
                handleOnClick(element.onClick)
            } label: {
                Text(element.text ?? "")
                    .font(.system(
                        size: element.style.fontSize ?? 16,
                        weight: fontWeight(from: element.style.fontWeight)
                    ))
                    .foregroundColor(element.style.textColor ?? .white)
                    .frame(maxWidth: .infinity)
                    .padding(edgeInsets(from: element.style.padding))
                    .background(element.style.backgroundColor ?? Color.blue)
                    .clipShape(
                        RoundedRectangle(cornerRadius: element.style.cornerRadius ?? 12)
                    )
            }
            .padding(edgeInsets(from: element.style.margin))

        case .spacer:
            Color.clear
                .frame(height: element.style.size?.height ?? 12)

        case .productsContainer:
            if let slots = content.products?.slots {
                switch element.style.productContainerType ?? .row {
                case .row:
                    ScrollView(.horizontal, showsIndicators: false) {
                        LazyHStack(spacing: 12) {
                            ForEach(Array(slots.enumerated()), id: \.offset) { itemIndex, slot in
                                productCard(
                                    slot: slot,
                                    key: productKey(slot: slot, index: itemIndex),
                                    content: content,
                                    campaign: campaign
                                )
                            }
                        }
                        .padding(edgeInsets(from: element.style.padding))
                    }
                    .padding(edgeInsets(from: element.style.margin))

                case .grid:
                    let columns = Array(
                        repeating: GridItem(.flexible(), spacing: 12),
                        count: max(element.style.gridColumns ?? 3, 1)
                    )

                    LazyVGrid(columns: columns, spacing: 12) {
                        ForEach(Array(slots.enumerated()), id: \.offset) { itemIndex, slot in
                            productCard(
                                slot: slot,
                                key: productKey(slot: slot, index: itemIndex),
                                content: content,
                                campaign: campaign
                            )
                        }
                    }
                    .padding(edgeInsets(from: element.style.padding))
                    .padding(edgeInsets(from: element.style.margin))
                }
            }

        default:
            EmptyView()
        }
    }

    @ViewBuilder
    private func productCard(
        slot: Slot,
        key: String,
        content: CampaignContent,
        campaign: Campaign
    ) -> some View {
        VisibilityObserver {
            sendProductVisibleImpressionIfNeeded(
                for: key,
                slot: slot,
                content: content,
                campaign: campaign
            )
        } content: {
            VStack(alignment: .leading, spacing: 8) {
                if let imageUrl = stringValue("imageUrl", in: slot.item) {
                    RemoteImageView(urlString: imageUrl, height: 160)
                }

                Text(stringValue("name", in: slot.item) ?? "Unknown product")
                    .font(.headline)
                    .foregroundColor(.primary)

                if let price = stringValue("price", in: slot.item) {
                    Text(price)
                        .font(.subheadline)
                        .foregroundColor(.secondary)
                }
            }
            .padding(12)
            .frame(maxWidth: .infinity, alignment: .leading)
            .background(Color.white)
            .clipShape(RoundedRectangle(cornerRadius: 16))
            .onTapGesture {
                GravitySDK.instance.sendProductEngagement(
                    engagement: ProductClickEngagement(
                        slot: slot,
                        content: content,
                        campaign: campaign
                    )
                )
                onOpenProduct(slot.item)
            }
        }
    }

    @MainActor
    private func loadContent() async {
        isLoading = true
        defer { isLoading = false }

        let response = await GravitySDK.instance.getContentBySelector(
            selector: selector,
            pageContext: pageContext
        )

        campaign = response?.data.first
        content = campaign?.payload.first?.contents.first
        hasSentContentImpression = false
        hasSentContentVisibleImpression = false
        visibleProductKeys.removeAll()
    }

    private func sendContentImpressionIfNeeded(
        content: CampaignContent,
        campaign: Campaign
    ) {
        guard !hasSentContentImpression else { return }
        hasSentContentImpression = true

        GravitySDK.instance.sendContentEngagement(
            engagement: ContentImpressionEngagement(
                content: content,
                campaign: campaign
            )
        )
    }

    private func sendContentVisibleImpressionIfNeeded(
        content: CampaignContent,
        campaign: Campaign
    ) {
        guard !hasSentContentVisibleImpression else { return }
        hasSentContentVisibleImpression = true

        GravitySDK.instance.sendContentEngagement(
            engagement: ContentVisibleImpressionEngagement(
                content: content,
                campaign: campaign
            )
        )
    }

    private func sendProductVisibleImpressionIfNeeded(
        for key: String,
        slot: Slot,
        content: CampaignContent,
        campaign: Campaign
    ) {
        guard !visibleProductKeys.contains(key) else { return }
        visibleProductKeys.insert(key)

        GravitySDK.instance.sendProductEngagement(
            engagement: ProductVisibleImpressionEngagement(
                slot: slot,
                content: content,
                campaign: campaign
            )
        )
    }

    private func handleOnClick(_ onClick: OnClickModel?) {
        guard let onClick else { return }

        switch onClick.action {
        case .followUrl:
            guard
                let rawUrl = onClick.url,
                let url = URL(string: rawUrl)
            else { return }
            onOpenUrl(url)

        case .followDeeplink:
            guard let deeplink = onClick.deeplink else { return }
            onOpenDeeplink(deeplink)

        case .copy:
            UIPasteboard.general.string = onClick.copyData

        default:
            break
        }
    }

    private func productKey(slot: Slot, index: Int) -> String {
        slot.slotId
            ?? stringValue("sku", in: slot.item)
            ?? stringValue("id", in: slot.item)
            ?? "slot-\(index)"
    }

    private func stringValue(_ key: String, in item: [String: Any?]) -> String? {
        if let value = item[key] as? String {
            return value
        }
        if let value = item[key] as? NSNumber {
            return value.stringValue
        }
        return nil
    }

    private func edgeInsets(from padding: GravityPadding?) -> EdgeInsets {
        EdgeInsets(
            top: padding?.top ?? 0,
            leading: padding?.left ?? 0,
            bottom: padding?.bottom ?? 0,
            trailing: padding?.right ?? 0
        )
    }

    private func edgeInsets(from margin: GravityMargin?) -> EdgeInsets {
        EdgeInsets(
            top: margin?.top ?? 0,
            leading: margin?.left ?? 0,
            bottom: margin?.bottom ?? 0,
            trailing: margin?.right ?? 0
        )
    }

    private func alignment(from value: GravityContentAlignment?) -> Alignment {
        switch value {
        case .center:
            return .center
        case .end:
            return .trailing
        default:
            return .leading
        }
    }

    private func horizontalAlignment(from value: GravityContentAlignment?) -> HorizontalAlignment {
        switch value {
        case .center:
            return .center
        case .end:
            return .trailing
        default:
            return .leading
        }
    }

    private func fontWeight(from value: Double?) -> Font.Weight {
        switch value {
        case 700:
            return .bold
        case 600:
            return .semibold
        case 500:
            return .medium
        default:
            return .regular
        }
    }
}

@available(iOS 15.0, *)
private struct RemoteImageView: View {
    let urlString: String
    let height: Double

    var body: some View {
        if let url = URL(string: urlString) {
            SwiftUI.AsyncImage(url: url) { phase in
                switch phase {
                case .success(let image):
                    image
                        .resizable()
                        .scaledToFill()
                default:
                    ZStack {
                        Color.gray.opacity(0.1)
                        ProgressView()
                    }
                }
            }
            .frame(height: height)
            .clipShape(RoundedRectangle(cornerRadius: 12))
        }
    }
}

private struct VisibilityObserver<Content: View>: View {
    let onVisible: () -> Void
    let content: () -> Content

    @State private var hasFired = false
    @State private var workItem: DispatchWorkItem?

    var body: some View {
        content()
            .background(
                GeometryReader { geometry in
                    Color.clear
                        .onAppear {
                            evaluate(frame: geometry.frame(in: .global))
                        }
                        .onChange(of: geometry.frame(in: .global)) { newFrame in
                            evaluate(frame: newFrame)
                        }
                        .onDisappear {
                            workItem?.cancel()
                            workItem = nil
                        }
                }
            )
    }

    private func evaluate(frame: CGRect) {
        guard !hasFired else { return }

        let screen = UIScreen.main.bounds
        let intersection = screen.intersection(frame)
        let visibleArea = intersection.width * intersection.height
        let totalArea = frame.width * frame.height

        guard totalArea > 0 else { return }

        if visibleArea < totalArea * 0.5 {
            workItem?.cancel()
            workItem = nil
            return
        }

        workItem?.cancel()
        let task = DispatchWorkItem {
            hasFired = true
            onVisible()
        }
        workItem = task
        DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: task)
    }
}
