use crate::{
    helpers::{doc_tokens, payload_type_expr, serialize_type, serialize_type_decl},
    naming::{escape_ident, to_camel},
    type_generator::TypeGenerator,
};
use genco::prelude::*;
use js::Tokens;
use sails_idl_ast::codec::has_scale_codec;
use sails_idl_parser_v2::ast;

pub(crate) struct ServiceGenerator<'a> {
    type_gen: &'a TypeGenerator,
}

impl<'a> ServiceGenerator<'a> {
    pub(crate) fn new(type_gen: &'a TypeGenerator) -> Self {
        Self { type_gen }
    }

    pub(crate) fn render(
        &self,
        tokens: &mut Tokens,
        service_expo: &ast::ServiceExpo,
        service: &ast::ServiceUnit,
    ) {
        let class_name = service_expo.name.name.to_string();
        let interface_id = service
            .name
            .interface_id
            .expect("Service must have interface_id")
            .to_string();

        self.type_gen.render_all(tokens, &service.types);

        let mut all_types = Vec::new();
        for ty in &service.types {
            all_types.push(serialize_type(ty));
        }
        let resolver_types = format!("[{}]", all_types.join(", "));

        let gear_api = &js::import("@gear-js/api", "GearApi");
        let hex_string = &js::import("@gear-js/api", "HexString");
        let type_resolver = &js::import("sails-js", "TypeResolver");

        let func_tokens = service
            .funcs
            .iter()
            .filter(|func| has_scale_codec(&func.annotations))
            .map(|func| self.render_func(func, func.entry_id));

        let event_tokens = service
            .events
            .iter()
            .filter(|event| has_scale_codec(&event.annotations))
            .map(|event| self.render_event(event, event.entry_id));

        let extend_tokens = service.extends.iter().map(|base| {
            let base_class_name = base.name.clone();
            let accessor_name = escape_ident(&to_camel(&base.name));

            quote! {
                public get $(accessor_name)(): $(&base_class_name) {
                  return new $(&base_class_name)(this._api, this._programId, this._routeIdx);
                }
            }
        });
        let interface_id_type = &js::import("sails-js-parser-idl-v2", "InterfaceId");

        quote_in! { *tokens =>
            export class $class_name {
              private _typeResolver: $type_resolver;
              constructor(
                private _api: $gear_api,
                private _programId: $hex_string,
                private _routeIdx: number = 0,
              ) {
                this._typeResolver = new $type_resolver($resolver_types);
              }
              private get registry() {
                return this._typeResolver.registry;
              }
              public get interfaceId(): $interface_id_type {
                return $interface_id_type.from($(quoted(&interface_id)));
              }
              $(for ext in extend_tokens => $ext$['\n'])
              $(for func in func_tokens => $func$['\n'])
              $(for event in event_tokens => $event$['\n'])
            }
        };
    }

    fn render_func(&self, func: &ast::ServiceFunc, entry_id: u16) -> Tokens {
        let method_name = escape_ident(&to_camel(&func.name));

        let args = func.params.iter().map(|p| {
            let ident = escape_ident(&p.name);
            let ty = self.type_gen.ts_type_decl(&p.type_decl);
            quote!($(ident): $(ty))
        });

        let return_type = if let Some(throws) = &func.throws {
            let ok = self.type_gen.ts_type_decl(&func.output);
            let err = self.type_gen.ts_type_decl(throws);
            quote!({ ok: $(ok) } | { err: $(err) })
        } else {
            self.type_gen.ts_type_decl(&func.output)
        };

        let payload_type = payload_type_expr(&func.params, "this._typeResolver");

        let return_type_scale = format!(
            "this._typeResolver.getTypeDeclString({})",
            serialize_type_decl(&func.output)
        );

        let params_expr = if func.params.is_empty() {
            "null".to_string()
        } else if func.params.len() == 1 {
            escape_ident(&func.params[0].name)
        } else {
            format!(
                "[{}]",
                func.params
                    .iter()
                    .map(|p| escape_ident(&p.name))
                    .collect::<Vec<_>>()
                    .join(", ")
            )
        };

        let doc_tokens = doc_tokens(&func.docs);

        let query_builder = &js::import("sails-js", "QueryBuilderWithHeader");
        let tx_builder = &js::import("sails-js", "TransactionBuilderWithHeader");
        let message_header = &js::import("sails-js-parser-idl-v2", "SailsMessageHeader");

        match func.kind {
            ast::FunctionKind::Query => {
                quote! {
                    $doc_tokens
                    public $method_name($(for arg in args join (, ) => $arg)): $query_builder<$(&return_type)> {
                      return new $query_builder<$(&return_type)>(
                        this._api,
                        this.registry,
                        this._programId,
                        $message_header.v1(this.interfaceId, $(entry_id), this._routeIdx),
                        $params_expr,
                        $payload_type,
                        $return_type_scale,
                      );
                    }
                }
            }
            ast::FunctionKind::Command => {
                quote! {
                    $doc_tokens
                    public $method_name($(for arg in args join (, ) => $arg)): $tx_builder<$(&return_type)> {
                      return new $tx_builder<$(&return_type)>(
                        this._api,
                        this.registry,
                        $(quoted("send_message")),
                        $message_header.v1(this.interfaceId, $(entry_id), this._routeIdx),
                        $params_expr,
                        $payload_type,
                        $return_type_scale,
                        this._programId,
                      );
                    }
                }
            }
        }
    }

    fn render_event(&self, event: &ast::ServiceEvent, entry_id: u16) -> Tokens {
        let method_name = escape_ident(&format!("subscribeTo{}Event", event.name));
        let event_ts_type = self.type_gen.ts_struct_def_tokens(&event.def);
        let type_str = "`([u8; 16], ${typeStr})`";

        let zero_address = &js::import("sails-js", "ZERO_ADDRESS");
        let message_header = &js::import("sails-js-parser-idl-v2", "SailsMessageHeader");
        let struct_field = &js::import("sails-js-types", "IStructField");
        let doc_tokens = doc_tokens(&event.docs);

        quote! {
            $doc_tokens
            public $(method_name)<T = $(&event_ts_type)>(callback: (eventData: T) => void | Promise<void>): Promise<() => void> {
              const interfaceIdu64 = this.interfaceId.asU64();
              const eventFields = $(event.def.to_json_string().expect("StructDef should be serializable to JSON")).fields as $struct_field[];
              const typeStr = this._typeResolver.getStructDef(eventFields, {}, true);
              return this._api.gearEvents.subscribeToGearEvent("UserMessageSent", ({ data: { message } }) => {
                if (!message.source.eq(this._programId)) return;
                if (!message.destination.eq($zero_address)) return;

                const { ok, header } = $message_header.tryFromBytes(message.payload);
                if (ok && header.interfaceId.asU64() === interfaceIdu64 && header.entryId === $(entry_id)) {
                  callback(this.registry.createType($type_str, message.payload)[1].toJSON() as T);
                }
              });
            }
        }
    }
}
