import Foundation

/// Synor Governance SDK client for Swift.
///
/// Provides proposals, voting, DAOs, and vesting operations.
public class SynorGovernance {
    private let config: GovernanceConfig
    private let session: URLSession
    private let decoder: JSONDecoder
    private let encoder: JSONEncoder
    private var closed = false

    private static let finalStatuses: Set<ProposalStatus> = [.passed, .rejected, .executed, .cancelled]

    public init(config: GovernanceConfig) {
        self.config = config

        let sessionConfig = URLSessionConfiguration.default
        sessionConfig.timeoutIntervalForRequest = config.timeout
        sessionConfig.timeoutIntervalForResource = config.timeout * 2
        self.session = URLSession(configuration: sessionConfig)

        self.decoder = JSONDecoder()
        self.encoder = JSONEncoder()
    }

    // MARK: - Proposal Operations

    /// Create a proposal.
    public func createProposal(_ proposal: ProposalDraft) async throws -> Proposal {
        var body: [String: Any] = [
            "title": proposal.title,
            "description": proposal.description
        ]
        if let url = proposal.discussionUrl { body["discussionUrl"] = url }
        if let start = proposal.votingStartTime { body["votingStartTime"] = start }
        if let end = proposal.votingEndTime { body["votingEndTime"] = end }
        if let daoId = proposal.daoId { body["daoId"] = daoId }
        if let actions = proposal.actions {
            body["actions"] = actions.map { ["target": $0.target, "method": $0.method, "data": $0.data, "value": $0.value as Any] }
        }
        return try await post(path: "/proposals", body: body)
    }

    /// Get proposal by ID.
    public func getProposal(proposalId: String) async throws -> Proposal {
        return try await get(path: "/proposals/\(proposalId.urlEncoded)")
    }

    /// List proposals.
    public func listProposals(filter: ProposalFilter? = nil) async throws -> [Proposal] {
        var params: [String] = []
        if let status = filter?.status { params.append("status=\(status.rawValue)") }
        if let proposer = filter?.proposer { params.append("proposer=\(proposer.urlEncoded)") }
        if let daoId = filter?.daoId { params.append("daoId=\(daoId.urlEncoded)") }
        if let limit = filter?.limit { params.append("limit=\(limit)") }
        if let offset = filter?.offset { params.append("offset=\(offset)") }

        let path = params.isEmpty ? "/proposals" : "/proposals?\(params.joined(separator: "&"))"
        let response: ProposalsResponse = try await get(path: path)
        return response.proposals ?? []
    }

    /// Cancel a proposal.
    public func cancelProposal(proposalId: String) async throws -> Proposal {
        return try await post(path: "/proposals/\(proposalId.urlEncoded)/cancel", body: [:] as [String: String])
    }

    /// Execute a passed proposal.
    public func executeProposal(proposalId: String) async throws -> Proposal {
        return try await post(path: "/proposals/\(proposalId.urlEncoded)/execute", body: [:] as [String: String])
    }

    /// Wait for proposal to reach a final state.
    public func waitForProposal(proposalId: String, pollInterval: TimeInterval = 60, maxWait: TimeInterval = 604800) async throws -> Proposal {
        let deadline = Date().addingTimeInterval(maxWait)
        while Date() < deadline {
            let proposal = try await getProposal(proposalId: proposalId)
            if Self.finalStatuses.contains(proposal.status) { return proposal }
            try await Task.sleep(nanoseconds: UInt64(pollInterval * 1_000_000_000))
        }
        throw GovernanceError(message: "Timeout waiting for proposal completion")
    }

    // MARK: - Voting Operations

    /// Vote on a proposal.
    public func vote(proposalId: String, vote: Vote, weight: String? = nil) async throws -> VoteReceipt {
        var body: [String: Any] = ["choice": vote.choice.rawValue]
        if let reason = vote.reason { body["reason"] = reason }
        if let weight = weight { body["weight"] = weight }
        return try await post(path: "/proposals/\(proposalId.urlEncoded)/vote", body: body)
    }

    /// Get votes for a proposal.
    public func getVotes(proposalId: String) async throws -> [VoteReceipt] {
        let response: VotesResponse = try await get(path: "/proposals/\(proposalId.urlEncoded)/votes")
        return response.votes ?? []
    }

    /// Get my vote on a proposal.
    public func getMyVote(proposalId: String) async throws -> VoteReceipt {
        return try await get(path: "/proposals/\(proposalId.urlEncoded)/votes/me")
    }

    /// Delegate voting power.
    public func delegate(delegatee: String, amount: String? = nil) async throws -> DelegationReceipt {
        var body: [String: Any] = ["delegatee": delegatee]
        if let amount = amount { body["amount"] = amount }
        return try await post(path: "/voting/delegate", body: body)
    }

    /// Undelegate voting power.
    public func undelegate(delegatee: String) async throws -> DelegationReceipt {
        return try await post(path: "/voting/undelegate", body: ["delegatee": delegatee])
    }

    /// Get voting power for an address.
    public func getVotingPower(address: String) async throws -> VotingPower {
        return try await get(path: "/voting/power/\(address.urlEncoded)")
    }

    /// Get delegations for an address.
    public func getDelegations(address: String) async throws -> [DelegationReceipt] {
        let response: DelegationsResponse = try await get(path: "/voting/delegations/\(address.urlEncoded)")
        return response.delegations ?? []
    }

    // MARK: - DAO Operations

    /// Create a DAO.
    public func createDao(_ daoConfig: DaoConfig) async throws -> Dao {
        var body: [String: Any] = [
            "name": daoConfig.name,
            "description": daoConfig.description,
            "type": daoConfig.type.rawValue,
            "votingPeriodDays": daoConfig.votingPeriodDays,
            "timelockDays": daoConfig.timelockDays
        ]
        if let token = daoConfig.tokenAddress { body["tokenAddress"] = token }
        if let quorum = daoConfig.quorumPercent { body["quorumPercent"] = quorum }
        if let threshold = daoConfig.proposalThreshold { body["proposalThreshold"] = threshold }
        if let members = daoConfig.multisigMembers { body["multisigMembers"] = members }
        if let threshold = daoConfig.multisigThreshold { body["multisigThreshold"] = threshold }
        return try await post(path: "/daos", body: body)
    }

    /// Get DAO by ID.
    public func getDao(daoId: String) async throws -> Dao {
        return try await get(path: "/daos/\(daoId.urlEncoded)")
    }

    /// List DAOs.
    public func listDaos(limit: Int? = nil, offset: Int? = nil) async throws -> [Dao] {
        var params: [String] = []
        if let limit = limit { params.append("limit=\(limit)") }
        if let offset = offset { params.append("offset=\(offset)") }
        let path = params.isEmpty ? "/daos" : "/daos?\(params.joined(separator: "&"))"
        let response: DaosResponse = try await get(path: path)
        return response.daos ?? []
    }

    /// Get DAO treasury.
    public func getDaoTreasury(daoId: String) async throws -> DaoTreasury {
        return try await get(path: "/daos/\(daoId.urlEncoded)/treasury")
    }

    /// Get DAO members.
    public func getDaoMembers(daoId: String) async throws -> [String] {
        let response: MembersResponse = try await get(path: "/daos/\(daoId.urlEncoded)/members")
        return response.members ?? []
    }

    // MARK: - Vesting Operations

    /// Create a vesting schedule.
    public func createVestingSchedule(_ schedule: VestingSchedule) async throws -> VestingContract {
        let body: [String: Any] = [
            "beneficiary": schedule.beneficiary,
            "totalAmount": schedule.totalAmount,
            "startTime": schedule.startTime,
            "cliffDuration": schedule.cliffDuration,
            "vestingDuration": schedule.vestingDuration,
            "revocable": schedule.revocable
        ]
        return try await post(path: "/vesting", body: body)
    }

    /// Get vesting contract.
    public func getVestingContract(contractId: String) async throws -> VestingContract {
        return try await get(path: "/vesting/\(contractId.urlEncoded)")
    }

    /// List vesting contracts.
    public func listVestingContracts(beneficiary: String? = nil) async throws -> [VestingContract] {
        let path = beneficiary != nil ? "/vesting?beneficiary=\(beneficiary!.urlEncoded)" : "/vesting"
        let response: VestingContractsResponse = try await get(path: path)
        return response.contracts ?? []
    }

    /// Claim vested tokens.
    public func claimVested(contractId: String) async throws -> ClaimReceipt {
        return try await post(path: "/vesting/\(contractId.urlEncoded)/claim", body: [:] as [String: String])
    }

    /// Revoke vesting.
    public func revokeVesting(contractId: String) async throws -> VestingContract {
        return try await post(path: "/vesting/\(contractId.urlEncoded)/revoke", body: [:] as [String: String])
    }

    /// Get releasable amount.
    public func getReleasableAmount(contractId: String) async throws -> String {
        let response: [String: String] = try await get(path: "/vesting/\(contractId.urlEncoded)/releasable")
        return response["amount"] ?? "0"
    }

    // MARK: - Lifecycle

    public func close() { closed = true }
    public var isClosed: Bool { closed }

    public func healthCheck() async -> Bool {
        do {
            let response: [String: String] = try await get(path: "/health")
            return response["status"] == "healthy"
        } catch { return false }
    }

    // MARK: - Private Methods

    private func get<T: Decodable>(path: String) async throws -> T {
        return try await request(method: "GET", path: path)
    }

    private func post<T: Decodable>(path: String, body: Any) async throws -> T {
        return try await request(method: "POST", path: path, body: body)
    }

    private func request<T: Decodable>(method: String, path: String, body: Any? = nil) async throws -> T {
        guard !closed else { throw GovernanceError(message: "Client has been closed") }

        var lastError: Error?
        for attempt in 0..<config.retries {
            do {
                return try await doRequest(method: method, path: path, body: body)
            } catch {
                lastError = error
                if attempt < config.retries - 1 {
                    try await Task.sleep(nanoseconds: UInt64(pow(2.0, Double(attempt))) * 1_000_000_000)
                }
            }
        }
        throw lastError ?? GovernanceError(message: "Unknown error")
    }

    private func doRequest<T: Decodable>(method: String, path: String, body: Any?) async throws -> T {
        guard let url = URL(string: config.endpoint + path) else { throw GovernanceError(message: "Invalid URL") }

        var request = URLRequest(url: url)
        request.httpMethod = method
        request.setValue("Bearer \(config.apiKey)", forHTTPHeaderField: "Authorization")
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue("swift/0.1.0", forHTTPHeaderField: "X-SDK-Version")

        if let body = body { request.httpBody = try JSONSerialization.data(withJSONObject: body) }

        let (data, response) = try await session.data(for: request)
        guard let httpResponse = response as? HTTPURLResponse else { throw GovernanceError(message: "Invalid response") }

        if httpResponse.statusCode >= 400 {
            let errorInfo = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
            throw GovernanceError(
                message: errorInfo?["message"] as? String ?? "HTTP \(httpResponse.statusCode)",
                code: errorInfo?["code"] as? String,
                statusCode: httpResponse.statusCode
            )
        }
        return try decoder.decode(T.self, from: data)
    }
}

extension String {
    fileprivate var urlEncoded: String {
        addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? self
    }
}
