// Copyright © 2024 Ryan Booker. All rights reserved.

import ArgumentParser
import Domain
import Foundation
import Toolbox

public struct Search: AsyncParsableCommand {
    public static let configuration = CommandConfiguration(
        commandName: "search",
        abstract: "Search for something (Beta, requires Kagi Business (Team) plan).",
        usage: "kg search \"Do androids dream of electric sheep?\""
    )

    @Argument(help: "Something to search for") 
    public var query: String
    
    @Option(help: "The maximum number of search results")
    public var limit: Int?

    var client = Client.liveValue

    enum CodingKeys: CodingKey {
        case query, limit
    }

    public init() {
        // Intentionally left empty
    }

    public mutating func run() async throws {
        do {
            let response = try await Client.liveValue.run(query: query, limit: limit)
            print(response)
        } catch {
            throw ValidationError(error.localizedDescription)
        }
    }
}

// MARK: - Processing

extension Search {
    struct Client {
        var run: @Sendable (
            _ query: String,
            _ limit: Int?
        ) async throws -> Response<[Either<SearchResult, RelatedResults>]>

        @inlinable
        func run(
            query: String,
            limit: Int?
        ) async throws -> Response<[Either<SearchResult, RelatedResults>]> {
            try await run(query, limit)
        }
    }
}

extension Search.Client {
    static let liveValue = Self(
        run: { query, limit in
            var queryItems: [URLQueryItem] = [.init(name: "q", value: query)]

            if let limit {
                queryItems.append(.init(name: "limit", value: "\(limit)"))
            }

            var request = try Endpoint.search.authorizedRequest
            request.url?.append(queryItems: queryItems)

            let (data, response) = try await URLSession.shared.data(for: request)

            do {
                return try JSONDecoder().decode(Response.self, from: data)
            } catch {
                #if DEBUG
                print(response)
                print(error)
                print("-----")
                #endif
                throw error
            }
        }
    )
}

// MARK: - Response

extension Search {
    public struct SearchResult: CustomStringConvertible, Decodable {
        public struct Thumbnail: Decodable {
            public var url: URL
            public var height: Int
            public var width: Int
        }

        public var rank: Int
        public var url: URL
        public var title: String
        public var snippet: String?
        public var published: Date?
        public var thumbnail: Thumbnail?

        public var description: String {
            [
                """
                \(rank): \(title)
                \(url)
                -----
                """,
                snippet.map {
                    """
                    \($0)
                    -----
                    """
                },
                published.map {
                    """
                    \($0.formatted(date: .long, time: .shortened))
                    -----
                    """
                }
            ]
            .compactMap { $0 }
            .joined(separator: "\n")
        }
    }

    public struct RelatedResults: CustomStringConvertible, Decodable {
        public var list: [String]

        public var description: String {
            """
            Possibly related searches:
            \(list.map { "- \($0)" })
            """
        }
    }
}

extension Array where Element == Either<Search.SearchResult, Search.RelatedResults> {
    public var description: String {
        """
        Results:

        \(map(\.description).joined(separator: "\n\n"))
        """
    }
}