import AppKit
import SwiftUI

// MARK: - Fetching (dynamic: page HTML + origin paths, then API fallbacks)

/// Resolves and downloads a site icon from the live website first, then public favicon services.
enum WebSiteFaviconFetcher {
    private static let userAgent =
        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15"

    /// Ordered attempts: same-origin files → HTML `<link rel="icon">` → third-party fallbacks.
    static func loadImage(for siteURL: URL) async -> NSImage? {
        guard let host = siteURL.host, !host.isEmpty else { return nil }

        let scheme = (siteURL.scheme?.lowercased() == "http") ? "http" : "https"
        let port = siteURL.port

        // 1) Common paths on the same host (true “from site” URLs).
        let rootPaths = [
            "/favicon.ico",
            "/favicon.png",
            "/apple-touch-icon.png",
            "/apple-touch-icon-precomposed.png",
            "/icon.png",
        ]
        for path in rootPaths {
            if let u = absoluteURL(scheme: scheme, host: host, port: port, path: path),
               let image = await fetchBitmap(from: u) {
                return image
            }
        }

        // 2) Parse the homepage (or path) for `<link rel="icon" …>` etc.
        let pageURLs = pageCandidates(for: siteURL, scheme: scheme, host: host, port: port)
        for page in pageURLs {
            if let discovered = await discoverIconURLs(fromHTMLPage: page) {
                let sorted = sortIconCandidates(discovered)
                var seenURLs = Set<String>()
                for u in sorted {
                    guard seenURLs.insert(u.absoluteString).inserted else { continue }
                    if let image = await fetchBitmap(from: u) {
                        return image
                    }
                }
            }
        }

        // 3) Third-party resolvers (not “direct” from host, but reliable fallbacks).
        let hostKey = host.lowercased()
        if let u = googleFaviconURL(host: hostKey, size: 128), let image = await fetchBitmap(from: u) {
            return image
        }
        if let u = URL(string: "https://icons.duckduckgo.com/ip3/\(hostKey).ico"),
           let image = await fetchBitmap(from: u) {
            return image
        }
        if let parent = registrableParentHost(hostKey), parent != hostKey {
            if let u = googleFaviconURL(host: parent, size: 128), let image = await fetchBitmap(from: u) {
                return image
            }
        }
        if let u = URL(string: "https://unavatar.io/\(hostKey)?fallback=false"),
           let image = await fetchBitmap(from: u) {
            return image
        }

        return nil
    }

    // MARK: URL building

    private static func absoluteURL(scheme: String, host: String, port: Int?, path: String) -> URL? {
        var c = URLComponents()
        c.scheme = scheme
        c.host = host
        c.port = port
        c.path = path.hasPrefix("/") ? path : "/" + path
        return c.url
    }

    private static func pageCandidates(for siteURL: URL, scheme: String, host: String, port: Int?) -> [URL] {
        var list: [URL] = []
        var seen = Set<String>()

        func add(_ url: URL?) {
            guard let url else { return }
            let key = url.absoluteString
            guard !seen.contains(key) else { return }
            seen.insert(key)
            list.append(url)
        }

        add(siteURL)
        add(absoluteURL(scheme: scheme, host: host, port: port, path: "/"))

        if scheme == "https",
           let httpRoot = absoluteURL(scheme: "http", host: host, port: port, path: "/") {
            add(httpRoot)
        }

        return list
    }

    private static func googleFaviconURL(host: String, size: Int) -> URL? {
        var c = URLComponents(string: "https://www.google.com/s2/favicons")!
        c.queryItems = [
            URLQueryItem(name: "domain", value: host),
            URLQueryItem(name: "sz", value: "\(size)"),
        ]
        return c.url
    }

    private static func registrableParentHost(_ host: String) -> String? {
        let labels = host.split(separator: ".")
        guard labels.count >= 3 else { return nil }
        let lastTwo = labels.suffix(2).joined(separator: ".")
        if ["co.uk", "com.au", "co.jp", "co.nz", "com.br"].contains(lastTwo) {
            guard labels.count >= 4 else { return nil }
            return labels.suffix(3).joined(separator: ".")
        }
        return labels.suffix(2).joined(separator: ".")
    }

    // MARK: HTML discovery

    private struct IconCandidate {
        let url: URL
        let priority: Int
    }

    private static func discoverIconURLs(fromHTMLPage pageURL: URL) async -> [IconCandidate]? {
        var request = URLRequest(url: pageURL)
        request.timeoutInterval = 14
        request.setValue(userAgent, forHTTPHeaderField: "User-Agent")
        request.setValue("en-US,en;q=0.9", forHTTPHeaderField: "Accept-Language")

        guard let (data, response) = try? await URLSession.shared.data(for: request),
              let http = response as? HTTPURLResponse,
              (200...299).contains(http.statusCode) else {
            return nil
        }

        let cap = min(data.count, 768_000)
        let snippet = Data(data.prefix(cap))
        guard let html = String(data: snippet, encoding: .utf8) ?? String(data: snippet, encoding: .isoLatin1) else {
            return nil
        }

        var found: [IconCandidate] = []

        let linkPattern = #"<link\s+([^>]*?)>"#
        guard let linkRegex = try? NSRegularExpression(pattern: linkPattern, options: [.caseInsensitive]) else {
            return nil
        }

        let range = NSRange(html.startIndex..., in: html)
        linkRegex.enumerateMatches(in: html, options: [], range: range) { match, _, _ in
            guard let match, match.range.location != NSNotFound,
                  let innerRange = Range(match.range(at: 1), in: html) else { return }

            let attrs = String(html[innerRange])
            guard let rel = attributeValue(named: "rel", in: attrs)?.lowercased() else { return }
            guard rel.contains("icon") || rel.contains("apple-touch") else { return }
            guard let hrefRaw = attributeValue(named: "href", in: attrs) else { return }

            let href = hrefRaw
                .replacingOccurrences(of: "&amp;", with: "&")
                .replacingOccurrences(of: "&lt;", with: "<")
                .replacingOccurrences(of: "&gt;", with: ">")

            guard let resolved = URL(string: href, relativeTo: pageURL)?.absoluteURL else { return }

            let sizes = attributeValue(named: "sizes", in: attrs)?.lowercased() ?? ""
            let px = largestDimension(fromSizes: sizes)
            let priority = iconPriority(rel: rel, pixelHint: px)

            found.append(IconCandidate(url: resolved, priority: priority))
        }

        return found.isEmpty ? nil : found
    }

    /// Pull `attr="value"` / `attr='value'` from a fragment of `<link …>`.
    private static func attributeValue(named name: String, in fragment: String) -> String? {
        let escaped = NSRegularExpression.escapedPattern(for: name)
        let patterns = [
            escaped + #"=["']([^"']*)["']"#,
            escaped + #"=([^\s>]+)"#,
        ]
        for pattern in patterns {
            guard let regex = try? NSRegularExpression(pattern: pattern, options: [.caseInsensitive]) else { continue }
            let range = NSRange(fragment.startIndex..., in: fragment)
            guard let m = regex.firstMatch(in: fragment, range: range),
                  m.numberOfRanges > 1,
                  let r = Range(m.range(at: 1), in: fragment) else { continue }
            let v = String(fragment[r]).trimmingCharacters(in: .whitespacesAndNewlines)
            if !v.isEmpty { return v }
        }
        return nil
    }

    private static func largestDimension(fromSizes sizes: String) -> Int {
        var best = 0
        for part in sizes.split(separator: " ") {
            let s = String(part)
            let nums = s.split(separator: "x")
            if nums.count == 2,
               let a = Int(nums[0].filter(\.isNumber)),
               let b = Int(nums[1].filter(\.isNumber)) {
                best = max(best, a, b)
            }
        }
        return best
    }

    private static func iconPriority(rel: String, pixelHint: Int) -> Int {
        var score = pixelHint
        if rel.contains("apple-touch") { score += 500 }
        if rel.contains("shortcut") { score += 20 }
        if rel == "icon" { score += 10 }
        return score
    }

    private static func sortIconCandidates(_ items: [IconCandidate]) -> [URL] {
        items.sorted { $0.priority > $1.priority }.map(\.url)
    }

    // MARK: Image fetch

    private static func fetchBitmap(from url: URL) async -> NSImage? {
        var request = URLRequest(url: url)
        request.timeoutInterval = 18
        request.setValue(userAgent, forHTTPHeaderField: "User-Agent")
        request.setValue("image/*,*/*;q=0.8", forHTTPHeaderField: "Accept")

        guard let (data, response) = try? await URLSession.shared.data(for: request),
              let http = response as? HTTPURLResponse,
              (200...299).contains(http.statusCode),
              data.count > 24 else {
            return nil
        }

        let mime = http.value(forHTTPHeaderField: "Content-Type")?.lowercased() ?? ""
        if mime.contains("svg") {
            return nil
        }

        guard let image = NSImage(data: data), !image.representations.isEmpty else {
            return nil
        }

        let w = image.size.width
        let h = image.size.height
        guard w >= 8, h >= 8 else { return nil }

        return image
    }
}

// MARK: - Cache (per normalized host)

actor FaviconImageCache {
    static let shared = FaviconImageCache()

    private var memory: [String: NSImage] = [:]

    func image(for siteURL: URL) async -> NSImage? {
        guard let hostKey = Self.normalizedHostKey(for: siteURL) else { return nil }
        if let cached = memory[hostKey] { return cached }

        if let image = await WebSiteFaviconFetcher.loadImage(for: siteURL) {
            memory[hostKey] = image
            return image
        }
        return nil
    }

    private static func normalizedHostKey(for siteURL: URL) -> String? {
        guard var host = siteURL.host?.lowercased(), !host.isEmpty else { return nil }
        if host.hasPrefix("www.") { host = String(host.dropFirst(4)) }
        return host
    }
}

// MARK: - View

/// Loads a remote favicon for `webURL`; optional bundled asset while loading or if fetch fails.
struct WebSiteFaviconView: View {
    let webURL: URL
    let size: CGFloat
    let assetIconName: String
    let fallbackSymbolName: String
    var iconPaddingFactor: CGFloat = 0.12

    @State private var remoteImage: NSImage?

    var body: some View {
        Group {
            if let remote = remoteImage {
                Image(nsImage: remote)
                    .resizable()
                    .interpolation(.high)
                    .scaledToFit()
                    .padding(size * iconPaddingFactor)
            } else if !assetIconName.isEmpty, let asset = NSImage(named: assetIconName) {
                Image(nsImage: asset)
                    .resizable()
                    .interpolation(.high)
                    .scaledToFit()
                    .padding(size * iconPaddingFactor)
            } else {
                Image(systemName: fallbackSymbolName)
                    .font(.system(size: size * 0.28, weight: .semibold))
                    .foregroundStyle(.white.opacity(0.92))
                    .padding(size * iconPaddingFactor)
            }
        }
        .frame(width: size, height: size)
        .task(id: webURL.absoluteString) {
            remoteImage = nil
            // If we have a bundled asset for this tile, prefer it and skip network fetches.
            if !assetIconName.isEmpty, NSImage(named: assetIconName) != nil {
                return
            }

            let img = await FaviconImageCache.shared.image(for: webURL)
            await MainActor.run { remoteImage = img }
        }
    }
}
