package tst

import (
	"fmt"
	"strings"

	. "git.eugeniocarvalho.dev/eugeniucarvalho/apicodegen/common" // . "git.eugeniocarvalho.dev/eugeniucarvalho/apicodegen/gen"
	TS "git.eugeniocarvalho.dev/eugeniucarvalho/gg/generators/typescript"
)

var (
	FormatMap map[string]string

	// TemplatesMethods = map[string]func(p *Project, method *Method) (*G.Statement, error){
	// 	"post":     GenCreateStmts,
	// 	"put":      GenUpdateStmts,
	// 	"delete":   GenDeleteStmts,
	// 	"get_one":  GenGetStmtsOne,
	// 	"get_list": GenGetStmtsList,
	// }

	angularServiceOption    = []TS.CodeInterface{}
	angularServiceOptionMap = map[string]string{}
)

func GenResources(p *Project) {
	var (
		// angularServiceStmtName    string
		angularResourceName string
		ServiceStmt         = "ServiceStmt"
		// angularServiceMethods     []TS.CodeInterface
		angularServiceStmtMethods []TS.CodeInterface
	)

	// imports(module)

	// module.Line().Export().Class().Id(ServiceStmt).Block(
	// 	TS.Public().Id("Url").Call(TS.Raw("u: string, options:{}")).Op(":").String().Block(
	// 		TS.Raw(""),
	// 	),
	// ).Line()
	module.Line().Raw(`
	export type HttpOptions = {
		headers?: HttpHeaders;
		params?: any;
		withCredentials?: boolean;
		id?: string;
	} & {
		[prop: string]: any;
	};
	  

	export class ServiceStmt {
		
		// constructor(protected api: `).Id(ApiClassName(p)).Raw(`) {}
		constructor(protected api: ApiInterface) {}
	
		public Url(u: string, opts: {}): string {
			
			(u.match(/{(\w+)}/gm)||[]).map(argExpression => {
				const
				prop = argExpression.replace(/({|})/gm,''),
				value = opts[prop];
				if (!value) { throw Error(`).Raw("`Url params '${prop}' is required`").Raw(`);}
				u = u.replace(argExpression, value);
			});
			return u;
		}
	
		_event<T = any>(url, opt: HttpOptions): Observable<EventSourcePolyfill> {
			opt = this.api.options(opt);
			const _url = new URL(this.Url(url, opt)), { params = null } = opt;
			if (params) {
				params.map.forEach((values, prop) => values.map(value => _url.searchParams.append(prop, value)));
			}
			return new Observable(o => o.next(new EventSourcePolyfill(
				_url.toString(),
				{ heartbeatTimeout: 86400000, headers: { Authorization: this.api.Auth.Token() } },
			))).pipe(take(1));
		}
		
		_get<T = any>(url, opt: HttpOptions):Observable<T> {
			opt = this.api.options(opt);
			return this.api.http.get<any>(this.Url(url, opt), opt).pipe(take(1));
		}
	
		_put<T = any>(url, entity: any, opt: HttpOptions):Observable<T> {
			opt = this.api.options(opt);
			return this.api.http.put<any>(this.Url(url, opt), entity, opt).pipe(take(1));
		}
		
		_patch<T = any>(url, entity: any, opt: HttpOptions):Observable<T> {
			opt = this.api.options(opt);
			return this.api.http.patch<any>(this.Url(url, opt), entity, opt).pipe(take(1));
		}
	
		_post<T = any>(url, entity: any, opt: HttpOptions):Observable<T> {
			opt = this.api.options(opt);
			return this.api.http.post<any>(this.Url(url, opt), entity, opt).pipe(take(1));
		}
	
		_delete<T = any>(url, opt: HttpOptions) {
			opt = this.api.options(opt);
			return this.api.http.delete<any>(this.Url(url, opt), opt).pipe(take(1));
		}
	}`).Line()

	for _, resource := range p.Resources {
		FormatMap = map[string]string{}
		// angularServiceMethods = []TS.CodeInterface{}
		angularServiceStmtMethods = []TS.CodeInterface{}
		angularResourceName = strings.Title(resource.ID) + "Service"

		module.Comment(resource.Description).Line()

		// angularServiceMethods = append(angularServiceMethods, TS.Constructor(
		// 	TS.Protected().Id("api").Op(":").Id(ApiClassName(p)),
		// ).Block(
		// // TS.This().Dot("Url").Op("="),
		// ))

		// angularServiceStmtMethods = append(angularServiceStmtMethods, TS.Public().Id("Url").Op(":").String())

		// angularServiceStmtMethods = append(angularServiceStmtMethods, TS.Constructor(
		// 	TS.Protected().Id("api").Op(":").Id(ApiClassName(p)),
		// 	TS.Protected().Id("options").Op(":").Id("ServiceOptions"),
		// ).Block(
		// 	// TS.Raw("this.url").Op("=").Lit(p.UrlFromMethod()).Endl(),
		// 	TS.Raw("super();"),
		// 	TS.Raw("if (this.options == null)").Block(
		// 		// TS.Raw("this.options = new ServiceOptions();"),
		// 		TS.Raw("this.options = {};"),
		// 	),
		// 	// TS.Super().Call(TS.Id("options")),
		// ))

		// angularServiceStmtName = angularResourceName + "Stmt"

		// angularServiceMethods = append(angularServiceMethods, TS.Id("Options").Params(
		// 	TS.Id("options?").Op(":").Id("ServiceOptions"),
		// ).Op(":").Id(angularServiceStmtName).Block(
		// 	TS.Return().New().Id(angularServiceStmtName).Call(
		// 		TS.Id("this.api"),
		// 		TS.Id("options"),
		// 	),
		// ).Line())

		// angularServiceStmtMethods = append(angularServiceStmtMethods, TS.Line().Raw(`
		// options(opts?: HttpOptions): this {
		// 	opts && Object.assign(this.opts, opts)
		// 	return this;
		// }`).Line())

		for _, method := range resource.Methods {
			// if method.Template == "implement" {
			// 	continue
			// }

			GenAngularMethodStmt(
				p,
				resource,
				method,
				&angularServiceStmtMethods,
				angularResourceName,
			)

			// Vai ser usado no metodo de construcao da classe chamado em createAngularService
			for paramName, param := range method.Parameters {

				// fmt.Println("param >>>>>>>>>", resource.ID, method.ID, paramName)

				if _, find := angularServiceOptionMap[paramName]; find {
					continue
				}

				angularServiceOptionMap[paramName] = param.Location
				angularServiceOption = append(angularServiceOption,
					TS.Comment(param.Description).Id(paramName+"?").Op(":").Id(TS.ConvType(param.Type)).Endl(),
				)
			}
		}

		module.Line().Export().Class().Id(
			angularResourceName,
		// ).Extends().Id(fmt.Sprintf("%s<%s>",ServiceStmt, resource.Entity )).Block(
		).Extends().Id(fmt.Sprintf("%s",ServiceStmt )).Block(
			angularServiceStmtMethods...,
		).Line()

		// module.Line().Export().Class().Id(
		// 	angularResourceName,
		// ).Block(
		// 	angularServiceMethods...,
		// ).Line()
	}

	// Cria o arquivo de servido da api
	createAngularService(p, module)
	// Fim do laco de recurso
	// Salva o arquivo do modulo

}

func ApiClassName(p *Project) string {
	return p.Name + "Api"
}

func createAngularService(p *Project, module *TS.File) error {
	// var (
	// service = Ts.NewFile("api.service.ts")
	// )

	// Define a class que representa uma resposta da api
	// "Class gerencia todos os serviços da api.",
	module.Export().Interface().Id("ServiceResponse<T = any>").Block(
		TS.Raw("resultSizeEstimate: number;"),
		TS.Raw("nextPageToken: string;"),
		TS.Raw("entity?:T;"),
		TS.Raw("itens?:T[];"),
	)

	// Define todos os parametros atribuiveis as opcoes de uma requisicao da api
	module.Line().Export().Interface().Id("ServiceOptions").BlockFun(func(g *TS.Group) {
		g.Stmts = angularServiceOption
	})

	//
	createClientClass(p, module)
	//
	createAuthClass(p, module)

	module.Line().Comment("Objeto acessivel pelo usuario para realizar requisicoes à api.").Raw(`
	@Injectable({ providedIn: 'root' })
	// @Injectable()
	export class `).Raw(ApiClassName(p)).Raw(` {
		public Client: Client;
		// public BASE_URL = environment.BASE_URL;
		public Auth: Auth;
		// public queryParams: any = {};
		public poolReady = [];

		public httpOptions = {
			headers: new HttpHeaders({
				'Content-Type': 'application/json',
				'Accept': 'application/json'
			}),
			withCredentials: true,
		};

		constructor(
			public http: HttpClient,
			// public route: ActivatedRoute,
			// public router: Router,
			// @Inject(ApiOptionsParams) public apiOptions: `).Raw(ApiClassName(p) + "Options").Raw(`,
			public apiOptions: `).Raw(ApiClassName(p) + "Options").Raw(`,
		) {
			console.log('apiOptions `).Raw(ApiClassName(p)).Raw(`',apiOptions);
			// this.route.queryParams.subscribe(params => { this.queryParams = params; });
			this.Client = new Client(this);

			if (Boolean(window[oauthWindowProp])) {
				this.Auth = window[oauthWindowProp];
			} else {
				this.Auth = window[oauthWindowProp] = new Auth(this);
			}
		}

		onready(fn: () => void) {
			this.Auth.onAuthorize$.pipe(filter(grant => Boolean(grant))).subscribe(fn);
			// if (this.Auth.grant) {
			// 	fn();
			// } else {
			// 	this.poolReady.push(fn);
			// }
		}

		// options(opts?: HttpOptions): HttpOptions {
		// 	console.log({ ...this.httpOptions.headers });
		// 	opts = Object.assign(
		// 	  {
		// 		headers: new HttpHeaders({
		// 		  ...this.httpOptions.headers,
		// 		}),
		// 		params: {},
		// 	  },
		// 	  opts
		// 	);
		
		// 	// const headers = {
		// 	//   Authorization: this.Auth.Token(),
		// 	// };
		
		// 	opts.headers.set("Authorization", this.Auth.Token());
		
		// 	//this.copyHeader(this.httpOptions.headers, headers);
		// 	//this.copyHeader(opts.headers, headers);
		
		// 	const params = opts.params;
		// 	// Converte a query para base64
		// 	if (params.q) {
		// 	  params.q = window.btoa(unescape(encodeURIComponent(params.q)));
		// 	}
		
		// 	// delete opts.headers;
		// 	// delete opts.params;
		
		// 	// const options = Object.assign(
		// 	//   {
		// 	//     headers: new HttpHeaders(headers),
		// 	//     params: new HttpParams({ fromObject: params }),
		// 	//   },
		// 	//   opts
		// 	// );
		
		// 	// const options = Object.assign({
		// 	// 	headers: new HttpHeaders(hopts),
		// 	// 	params: {}
		// 	// }, opts);
		
		// 	// // Converte a query para base64
		// 	// if (options.params.q) {
		// 	// 	options.params.q = window.btoa(unescape(encodeURIComponent(options.params.q)));
		// 	// }
		
		// 	return opts;
		//   }
		  
		options(opts?: HttpOptions): HttpOptions {

			opts = Object.assign({
				headers: new HttpHeaders(),
				params: {},
			}, opts);
	
	
			const headers = {
				'Authorization': this.Auth.Token()
			};
	
			this.copyHeader(this.httpOptions.headers, headers);
			this.copyHeader(opts.headers, headers);
	
			const params = opts.params;
			// Converte a query para base64
			if (params.q) {
				params.q = window.btoa(unescape(encodeURIComponent(params.q)));
			}
	
			delete opts.headers;
			delete opts.params;
	
			const options = Object.assign({
				headers: new HttpHeaders(headers),
				params: new HttpParams({ fromObject: params }),
			}, opts);
	
	
			// const options = Object.assign({
			// 	headers: new HttpHeaders(hopts),
			// 	params: {}
			// }, opts);
	
			// // Converte a query para base64
			// if (options.params.q) {
			// 	options.params.q = window.btoa(unescape(encodeURIComponent(options.params.q)));
			// }
	
			return options;
		}
	
		copyHeader(headers, hopts) {
			if (!headers) { return; }
			headers.keys().map(key => { hopts[key] = headers.get(key); });
		}
	}`)
	// .Injetable("{providedIn: 'root'}").Export().Class().Id(ApiClassName(p)).Block(
	// 	TS.Public().Id("Client").Op(":").Id("Client").Endl(),
	// 	TS.Public().Id("baseURL").Op("=").Lit(p.BaseURL).Endl(),
	// 	TS.Public().Id("Auth").Op(":").Id("Auth").Endl(),
	// 	TS.Public().Id("httpOptions").Id("=").Block(
	// 		TS.Id("headers").Op(":").Raw("new HttpHeaders").Call(TS.Block(
	// 			TS.Raw("'Content-Type': 'application/json',"),
	// 			TS.Raw("'Accept': 'application/json'"),
	// 		)),
	// 	).Endl(),
	// 	// TS.Public().Id("http").Op(":").Id("HttpClient").Endl(),
	// 	TS.Constructor(
	// 		TS.Public().Id("http").Op(":").Id("HttpClient"),
	// 	).Block(
	// 		TS.This().Dot("Client").Op("=").New().Id("Client").Call(TS.This()).Endl(),
	// 		// TS.Id("this.http").Op("=").New().Id("Client").Call(TS.This()).Endl(),
	// 	),
	// 	TS.Id("Options").Params(
	// 		TS.Id("opts").Op(":").Id("ServiceOptions"),
	// 	).Op(":").Id("{headers: HttpHeaders,params: any}").Block(
	// 		TS.Return().Block(
	// 			TS.Raw("headers: this.httpOptions.headers,"),
	// 			// TS.Raw("params : opts.Params()"),
	// 			TS.Raw("params : opts"),
	// 		),
	// 	),
	// ).Line()

	module.Line().Raw(`

	export function ApiOptionsProvider(options) {
		console.log('ApiOptionsProvider', options);
		return options;
	}

	@NgModule({
		imports: [
		  CommonModule,
		  HttpClientModule,
		]
	  })
	`).Export().
		Class().
		Id(p.Name + "ApiModule").
		Block(
			TS.Raw(`
			static forRoot( options?: `).Raw(ApiClassName(p) + "Options").Raw(` ) : ModuleWithProviders {
			// static forRoot( options?:any ) : ModuleWithProviders {
				return({
					ngModule: `).Raw(p.Name + "ApiModule").Raw(`,
					providers: [
						{
							provide: ApiOptionsParams,
							useValue: options,
						},
						{
							provide: `).Raw(ApiClassName(p) + "Options").Raw(`,
							useFactory: ApiOptionsProvider,
							deps:[ApiOptionsParams]
						}
					]
				});
			}
			`),
		).
		Line()

	return nil
}

// createClientClass cria a classe de cliente que guarda a referencia para todos os servicos da api
func createClientClass(p *Project, file *TS.File) {
	var (
		angularResourceName      string
		angularClientStmts       = []TS.CodeInterface{}
		angularClientConstructor = []TS.CodeInterface{}
	)

	for _, resource := range p.Resources {

		angularResourceName = strings.Title(resource.ID) + "Service"

		angularClientStmts = append(
			angularClientStmts,
			TS.Public().Id(strings.Title(resource.ID)).Op(":").Id(angularResourceName).Endl(),
		)

		angularClientConstructor = append(
			angularClientConstructor,
			TS.This().Dot(strings.Title(resource.ID)).Op("=").New().Id(angularResourceName).Call(TS.Id("api")).Endl(),
		)
	}
	angularClientConstructor = append(
		angularClientConstructor,
		TS.Raw("this.Generic = new ServiceStmt").Call(TS.Id("api")).Endl(),
	)
	// angularClientStmts = append(
	// 	angularClientStmts,
	// 	TS.Raw(`public onready = new EventEmitter<any>();`),
	// )

	angularClientStmts = append(
		angularClientStmts,
		TS.Public().Id("Generic: ServiceStmt").Endl(),
	)

	angularClientStmts = append(
		angularClientStmts,
		TS.Constructor(
			TS.Protected().Id("api").Op(":").Id("ApiInterface"),
		).Block(angularClientConstructor...),
	)

	file.Line().Comment(
		"Class gerencia todos os serviços da api.",
	).Export().Class().Id("Client").Block(angularClientStmts...)

}


func GenAngularMethodStmt(p *Project, r *Resource, method *Method, methods *[]TS.CodeInterface, angularResourceName string) {

	var (
		typ        string
		params     = []TS.CodeInterface{}
		paramsHttp = []TS.CodeInterface{}
		// id         = method.ID
		// ret        = TS.Id("Observable")
		param         = "entity"
		template      string
		templateValue interface{}
		defined       bool
	)

	if templateValue, defined = method.Custom["ts.template.method"]; defined {
		template = templateValue.(string)
	} else {
		template = method.HttpMethod
	}

	template = strings.ToLower(template)

	switch template {

	case "post":
		typ = "post"
		params = append(params, TS.Id(param).Op(":").Id(method.Entity))
		// ret.TypeAliase(method.Entity)
	case "filter":
		typ = "get"
		// ret.TypeAliase(method.Entity)
		param = ""
		// params = append(params, TS.Id(param).Op(":").String())
	case "get":
		typ = "get"
		// ret.TypeAliase(method.Entity)
		param = ""
	case "put":
		typ = "put"
		// ret.TypeAliase(method.Entity)
		params = append(params, TS.Id(param).Op(":").Id(method.Entity).Op("|").String())
	case "delete":
		typ = "delete"
		// ret.TypeAliase(method.Entity)
		// params = append(params, TS.Id(param).Op(":").Id(method.Entity).Op("|").String())
	case "patch":
		typ = "patch"
		params = append(params, TS.Id(param).Op(":").Id(method.Entity).Op("|").String())
		// ret.TypeAliase(method.Entity)
		// params = append(params, TS.Id(param).Op(":").Id(method.Entity).Op("|").String())
	case "sse":
		typ = "event"

	default:
		panic(fmt.Sprintf("Method '%s' template not defined!", template))
	}

	path := method.Path
	for _, parameter := range method.Parameters {
		
	 alias := getCustom(parameter.Custom, "ts.api.alias")
	 if aliasString, ok := alias.(string);ok {
		path = strings.ReplaceAll(
			path,
			fmt.Sprintf("{%s}", parameter.ID),
			fmt.Sprintf("{%s}", aliasString),
		)
	 }
	}
	
	// paramsHttp = append(paramsHttp, TS.Raw("this.Url(url, opt.params)"))
	paramsHttp = append(paramsHttp, TS.Raw(fmt.Sprintf("`${this.api.apiOptions.BASE_URL}%s`", path)))

	switch typ {
	case "post", "put", "patch":
		paramsHttp = append(paramsHttp, TS.Id("entity"))
	}
	params = append(params, TS.Id("opt?: HttpOptions"))

	paramsHttp = append(paramsHttp, TS.Id("opt"))

	response := strings.Replace(method.Response, "*", "",-1)
	
	if response == ""  {
		response = method.Entity
	}

	// *methods = append(*methods, TS.Id(method.ID).Params(params...).Op(":").Id("Observable<ServiceResponse>").Block(
	*methods = append(*methods, TS.Id(method.ID).Params(params...).Block(
		// TS.Raw("let opt = this.api.Options(this.options)").Endl(),
		// TS.Raw("let url = ").Lit(p.GetUrlFromMethod(method)).Endl(),
		// TS.Raw("let url = ").Raw(fmt.Sprintf("`${this.api.BASE_URL}%s`", method.Path)).Endl(),
		TS.Line().Raw(
			fmt.Sprintf(
				"return this._%s<%s>",
				 typ,
				response,
			),
		).Call(paramsHttp...).Endl(),
		// TS.Line().Raw(fmt.Sprintf("return this._%s", typ )).Call(paramsHttp...).Endl(),
	).Line())
}

// func GenAngularMethod(p *Project, r *Resource, method *Method, methods *[]TS.CodeInterface, angularResourceName string) {
// 	params := []TS.CodeInterface{}
// 	param := "entity"
// 	methodId := method.ID

// 	callOptionsParam := ""

// 	switch strings.ToLower(method.HttpMethod) {
// 	case "post":
// 		params = append(params, TS.Id(param).Op(":").Id(method.Entity))
// 	case "filter":
// 		// methodId = "get"
// 		param = ""
// 	case "get":
// 		param = ""
// 		// params = append(params, TS.Id(param).Op(":").String())
// 	case "get_list":
// 		param = ""
// 		// params = append(params, TS.Id(param).Op(":").Id(method.Entity).Op("|").String())
// 	case "patch":
// 		param = ""
// 		params = append(params, TS.Id(param).Op(":").Id(method.Entity))
// 		// params = append(params, TS.Id(param).Op(":").Id(method.Entity).Op("|").String())
// 	case "put":
// 		params = append(params, TS.Id(param).Op(":").Id(method.Entity))
// 		callOptionsParam = "{id: entity.id}"
// 	case "delete":
// 		// params = append(params, TS.Id(param).Op(":").Id(method.Entity).Op("|").String())
// 		params = append(params, TS.Id(param).Op(":").Id(method.Entity))
// 		callOptionsParam = "{id: entity.id}"
// 		param = ""
// 	default:
// 		panic(fmt.Sprintf("O método http '%s' para o recurso %s não foi definido!", method.HttpMethod, method.ID))
// 	}

// 	// *methods = append(*methods, TS.Id(method.ID).Params(params...).Op(":").Id("Observable<ServiceResponse>").Block(
// 	*methods = append(*methods, TS.Id(method.ID).Params(params...).Op(":").Id("Observable<ServiceResponse>").Block(
// 		TS.Return().This().Dot("options").Call(TS.Raw(callOptionsParam)).Dot(methodId).Call(TS.Id(param)).Endl(),
// 	).Line())
// }
