# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt


import json

import frappe
from frappe import _
from frappe.contacts.doctype.contact.contact import get_full_name
from frappe.core.doctype.communication.email import make
from frappe.desk.form.load import get_attachments
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder import Order
from frappe.utils import get_url
from frappe.utils.print_format import download_pdf
from frappe.utils.user import get_user_fullname

from erpnext.accounts.party import _get_party_details, get_party_account_currency
from erpnext.buying.utils import validate_for_items
from erpnext.controllers.buying_controller import BuyingController
from erpnext.stock.doctype.material_request.material_request import set_missing_values

STANDARD_USERS = ("Guest", "Administrator")


class RequestforQuotation(BuyingController):
	# begin: auto-generated types
	# This code is auto-generated. Do not modify anything in this block.

	from typing import TYPE_CHECKING

	if TYPE_CHECKING:
		from frappe.types import DF

		from erpnext.buying.doctype.request_for_quotation_item.request_for_quotation_item import (
			RequestforQuotationItem,
		)
		from erpnext.buying.doctype.request_for_quotation_supplier.request_for_quotation_supplier import (
			RequestforQuotationSupplier,
		)

		amended_from: DF.Link | None
		billing_address: DF.Link | None
		billing_address_display: DF.TextEditor | None
		company: DF.Link
		email_template: DF.Link | None
		has_unit_price_items: DF.Check
		incoterm: DF.Link | None
		items: DF.Table[RequestforQuotationItem]
		letter_head: DF.Link | None
		message_for_supplier: DF.TextEditor | None
		mfs_html: DF.Code | None
		named_place: DF.Data | None
		naming_series: DF.Literal["PUR-RFQ-.YYYY.-"]
		opportunity: DF.Link | None
		schedule_date: DF.Date | None
		select_print_heading: DF.Link | None
		send_attached_files: DF.Check
		send_document_print: DF.Check
		shipping_address: DF.Link | None
		shipping_address_display: DF.TextEditor | None
		status: DF.Literal["", "Draft", "Submitted", "Cancelled"]
		subject: DF.Data
		suppliers: DF.Table[RequestforQuotationSupplier]
		tc_name: DF.Link | None
		terms: DF.TextEditor | None
		title: DF.Data | None
		transaction_date: DF.Date
		use_html: DF.Check
		vendor: DF.Link | None
	# end: auto-generated types

	def before_validate(self):
		self.set_has_unit_price_items()
		self.flags.allow_zero_qty = self.has_unit_price_items
		self.set_data_for_supplier()

	def validate(self):
		self.validate_duplicate_supplier()
		self.validate_supplier_list()
		super().validate_qty_is_not_zero()
		validate_for_items(self)
		super().set_qty_as_per_stock_uom()
		self.update_email_id()

		if self.docstatus < 1:
			# after amend and save, status still shows as cancelled, until submit
			self.db_set("status", "Draft")

	def set_has_unit_price_items(self):
		"""
		If permitted in settings and any item has 0 qty, the RFQ has unit price items.
		"""
		if not frappe.db.get_single_value("Buying Settings", "allow_zero_qty_in_request_for_quotation"):
			return

		self.has_unit_price_items = any(
			not row.qty for row in self.get("items") if (row.item_code and not row.qty)
		)

	def set_data_for_supplier(self):
		if self.email_template:
			data = frappe.get_value(
				"Email Template",
				self.email_template,
				["use_html", "response", "response_html", "subject"],
				as_dict=True,
			)

			self.use_html = data.use_html

			if data.use_html:
				if not self.mfs_html:
					self.mfs_html = data.response_html
			else:
				if not self.message_for_supplier:
					self.message_for_supplier = data.response

			if not self.subject:
				self.subject = data.subject

	def validate_duplicate_supplier(self):
		supplier_list = [d.supplier for d in self.suppliers]
		if len(supplier_list) != len(set(supplier_list)):
			frappe.throw(_("Same supplier has been entered multiple times"))

	def validate_supplier_list(self):
		for d in self.suppliers:
			prevent_rfqs = frappe.db.get_value("Supplier", d.supplier, "prevent_rfqs")
			if prevent_rfqs:
				standing = frappe.db.get_value("Supplier Scorecard", d.supplier, "status")
				frappe.throw(
					_("RFQs are not allowed for {0} due to a scorecard standing of {1}").format(
						d.supplier, standing
					)
				)
			warn_rfqs = frappe.db.get_value("Supplier", d.supplier, "warn_rfqs")
			if warn_rfqs:
				standing = frappe.db.get_value("Supplier Scorecard", d.supplier, "status")
				frappe.msgprint(
					_(
						"{0} currently has a {1} Supplier Scorecard standing, and RFQs to this supplier should be issued with caution."
					).format(d.supplier, standing),
					title=_("Caution"),
					indicator="orange",
				)

	def update_email_id(self):
		for rfq_supplier in self.suppliers:
			if not rfq_supplier.email_id:
				rfq_supplier.email_id = frappe.db.get_value("Contact", rfq_supplier.contact, "email_id")

	def validate_email_id(self, args):
		if not args.email_id:
			frappe.throw(
				_("Row {0}: For Supplier {1}, Email Address is Required to send an email").format(
					args.idx, frappe.bold(args.supplier)
				)
			)

	def on_submit(self):
		self.db_set("status", "Submitted")
		for supplier in self.suppliers:
			supplier.email_sent = 0
			supplier.quote_status = "Pending"
		self.send_to_supplier()

	def before_print(self, settings=None):
		"""Use the first suppliers data to render the print preview."""
		if self.vendor or not self.suppliers:
			# If a specific supplier is already set, via Tools > Download PDF,
			# we don't want to override it.
			return

		self.update_supplier_part_no(self.suppliers[0].supplier)

	def on_cancel(self):
		self.db_set("status", "Cancelled")

	@frappe.whitelist()
	def get_supplier_email_preview(self, supplier):
		"""Returns formatted email preview as string."""
		rfq_suppliers = list(filter(lambda row: row.supplier == supplier, self.suppliers))
		rfq_supplier = rfq_suppliers[0]

		self.validate_email_id(rfq_supplier)

		message = self.supplier_rfq_mail(rfq_supplier, "", self.get_link(), True)

		return message

	def send_to_supplier(self):
		"""Sends RFQ mail to involved suppliers."""
		for rfq_supplier in self.suppliers:
			if rfq_supplier.email_id is not None and rfq_supplier.send_email:
				self.validate_email_id(rfq_supplier)

				# make new user if required
				update_password_link, contact = self.update_supplier_contact(rfq_supplier, self.get_link())

				self.update_supplier_part_no(rfq_supplier.supplier)
				self.supplier_rfq_mail(rfq_supplier, update_password_link, self.get_link())
				rfq_supplier.email_sent = 1
				if not rfq_supplier.contact:
					rfq_supplier.contact = contact
				rfq_supplier.save()

	def get_link(self):
		# RFQ link for supplier portal
		route = frappe.db.get_value(
			"Portal Menu Item", {"reference_doctype": "Request for Quotation"}, ["route"]
		)
		if not route:
			frappe.throw(_("Please add Request for Quotation to the sidebar in Portal Settings."))

		return get_url(f"{route}/{self.name}")

	def update_supplier_part_no(self, supplier):
		self.vendor = supplier
		for item in self.items:
			item.supplier_part_no = frappe.db.get_value(
				"Item Supplier", {"parent": item.item_code, "supplier": supplier}, "supplier_part_no"
			)

	def update_supplier_contact(self, rfq_supplier, link):
		"""Create a new user for the supplier if not set in contact"""
		update_password_link, contact = "", ""

		if frappe.db.exists("User", rfq_supplier.email_id):
			user = frappe.get_doc("User", rfq_supplier.email_id)
		else:
			user, update_password_link = self.create_user(rfq_supplier, link)

		contact = self.link_supplier_contact(rfq_supplier, user)

		return update_password_link, contact

	def link_supplier_contact(self, rfq_supplier, user):
		"""If no Contact, create a new contact against Supplier. If Contact exists, check if email and user id set."""
		if rfq_supplier.contact:
			contact = frappe.get_doc("Contact", rfq_supplier.contact)
		else:
			contact = frappe.new_doc("Contact")
			contact.first_name = rfq_supplier.supplier_name or rfq_supplier.supplier
			contact.append("links", {"link_doctype": "Supplier", "link_name": rfq_supplier.supplier})
			contact.append("email_ids", {"email_id": user.name, "is_primary": 1})

		if not contact.email_id and not contact.user:
			contact.email_id = user.name
			contact.user = user.name

		contact.save(ignore_permissions=True)

		if rfq_supplier.supplier:
			self.update_user_in_supplier(rfq_supplier.supplier, user.name)

		if not rfq_supplier.contact:
			# return contact to later update, RFQ supplier row's contact
			return contact.name

	def update_user_in_supplier(self, supplier, user):
		"""Update user in Supplier."""
		if not frappe.db.exists("Portal User", {"parent": supplier, "user": user}):
			supplier_doc = frappe.get_doc("Supplier", supplier)
			supplier_doc.append(
				"portal_users",
				{
					"user": user,
				},
			)

			supplier_doc.flags.ignore_validate = True
			supplier_doc.flags.ignore_mandatory = True
			supplier_doc.flags.ignore_permissions = True

			supplier_doc.save()

	def create_user(self, rfq_supplier, link):
		contact_name = None
		if rfq_supplier.contact:
			name_fields = frappe.get_value(
				"Contact", rfq_supplier.contact, ["first_name", "middle_name", "last_name"]
			)
			if name_fields:
				contact_name = get_full_name(*name_fields)

		user = frappe.get_doc(
			{
				"doctype": "User",
				"send_welcome_email": 0,
				"email": rfq_supplier.email_id,
				"first_name": contact_name or rfq_supplier.supplier_name or rfq_supplier.supplier,
				"user_type": "Website User",
				"redirect_url": link,
			}
		)
		user.save(ignore_permissions=True)
		update_password_link = user._reset_password()

		return user, update_password_link

	def supplier_rfq_mail(self, data, update_password_link, rfq_link, preview=False):
		full_name = get_user_fullname(frappe.session["user"])
		if full_name == "Guest":
			full_name = "Administrator"

		doc_args = self.as_dict()

		if data.get("contact"):
			contact = frappe.get_doc("Contact", data.get("contact"))
			doc_args["contact"] = contact.as_dict()

		doc_args.update(
			{
				"supplier": data.get("supplier"),
				"supplier_name": data.get("supplier_name"),
				"update_password_link": f'<a href="{update_password_link}" class="btn btn-default btn-xs" target="_blank">{_("Set Password")}</a>',
				"portal_link": f'<a href="{rfq_link}" class="btn btn-default btn-xs" target="_blank"> {_("Submit your Quotation")} </a>',
				"user_fullname": full_name,
			}
		)

		fixed_procurement_email = frappe.db.get_single_value("Buying Settings", "fixed_email")
		if fixed_procurement_email:
			sender = frappe.db.get_value("Email Account", fixed_procurement_email, "email_id")
		else:
			sender = frappe.session.user not in STANDARD_USERS and frappe.session.user or None

		message_template = self.mfs_html if self.use_html else self.message_for_supplier
		# nosemgrep: frappe-semgrep-rules.rules.security.frappe-ssti
		rendered_message = frappe.render_template(message_template, doc_args, restrict_globals=True)

		subject_source = (
			self.subject
			or frappe.get_value("Email Template", self.email_template, "subject")
			or _("Request for Quotation")
		)
		rendered_subject = frappe.render_template(subject_source, doc_args, restrict_globals=True)
		if preview:
			return {
				"message": rendered_message,
				"subject": rendered_subject,
			}

		attachments = []
		if self.send_attached_files:
			attachments = self.get_attachments()

		if self.send_document_print:
			supplier_language = frappe.db.get_value("Supplier", data.supplier, "language")
			system_language = frappe.db.get_single_value("System Settings", "language")
			attachments.append(
				frappe.attach_print(
					self.doctype,
					self.name,
					doc=self,
					print_format=self.meta.default_print_format or "Standard",
					lang=supplier_language or system_language,
					letterhead=self.letter_head,
				)
			)

		self.send_email(
			data,
			sender,
			rendered_subject,
			rendered_message,
			attachments,
		)

	def send_email(self, data, sender, subject, message, attachments):
		make(
			subject=subject,
			content=message,
			recipients=data.email_id,
			sender=sender,
			attachments=attachments,
			send_email=True,
			doctype=self.doctype,
			name=self.name,
		)["name"]

		frappe.msgprint(_("Email Sent to Supplier {0}").format(data.supplier))

	def get_attachments(self):
		return [d.name for d in get_attachments(self.doctype, self.name)]

	def update_rfq_supplier_status(self, sup_name=None):
		for supplier in self.suppliers:
			if sup_name is None or supplier.supplier == sup_name:
				quote_status = _("Received")
				for item in self.items:
					sqi_count = frappe.db.sql(
						"""
						SELECT
							COUNT(sqi.name) as count
						FROM
							`tabSupplier Quotation Item` as sqi,
							`tabSupplier Quotation` as sq
						WHERE sq.supplier = %(supplier)s
							AND sqi.docstatus = 1
							AND sqi.request_for_quotation_item = %(rqi)s
							AND sqi.parent = sq.name""",
						{"supplier": supplier.supplier, "rqi": item.name},
						as_dict=1,
					)[0]
					if (sqi_count.count) == 0:
						quote_status = _("Pending")
				supplier.quote_status = quote_status


@frappe.whitelist()
def send_supplier_emails(rfq_name):
	check_portal_enabled("Request for Quotation")
	rfq = frappe.get_doc("Request for Quotation", rfq_name)
	if rfq.docstatus == 1:
		rfq.send_to_supplier()


def check_portal_enabled(reference_doctype):
	if not frappe.db.get_value("Portal Menu Item", {"reference_doctype": reference_doctype}, "enabled"):
		frappe.throw(
			_(
				"The Access to Request for Quotation From Portal is Disabled. To Allow Access, Enable it in Portal Settings."
			)
		)


def get_list_context(context=None):
	from erpnext.controllers.website_list_for_contact import get_list_context

	list_context = get_list_context(context)
	list_context.update(
		{
			"show_sidebar": True,
			"show_search": True,
			"no_breadcrumbs": True,
			"title": _("Request for Quotation"),
			"list_template": "templates/includes/list/list.html",
		}
	)
	return list_context


@frappe.whitelist()
def make_supplier_quotation_from_rfq(source_name, target_doc=None, for_supplier=None):
	def postprocess(source, target_doc):
		if for_supplier:
			target_doc.supplier = for_supplier
			args = _get_party_details(for_supplier, party_type="Supplier", ignore_permissions=True)
			target_doc.currency = args.currency or get_party_account_currency(
				"Supplier", for_supplier, source.company
			)
			target_doc.buying_price_list = args.buying_price_list or frappe.db.get_single_value(
				"Buying Settings", "buying_price_list"
			)
		set_missing_values(source, target_doc)

	doclist = get_mapped_doc(
		"Request for Quotation",
		source_name,
		{
			"Request for Quotation": {
				"doctype": "Supplier Quotation",
				"validation": {"docstatus": ["=", 1]},
				"field_map": {"opportunity": "opportunity"},
			},
			"Request for Quotation Item": {
				"doctype": "Supplier Quotation Item",
				"field_map": {
					"name": "request_for_quotation_item",
					"parent": "request_for_quotation",
					"project_name": "project",
				},
			},
		},
		target_doc,
		postprocess,
	)

	return doclist


# This method is used to make supplier quotation from supplier's portal.
@frappe.whitelist()
def create_supplier_quotation(doc):
	if isinstance(doc, str):
		doc = json.loads(doc)

	if frappe.session.user not in frappe.get_all(
		"Portal User", {"parent": doc.get("supplier")}, pluck="user"
	):
		frappe.throw(_("Not Permitted"), frappe.PermissionError)

	try:
		sq_doc = frappe.get_doc(
			{
				"doctype": "Supplier Quotation",
				"supplier": doc.get("supplier"),
				"terms": doc.get("terms"),
				"company": doc.get("company"),
				"currency": doc.get("currency")
				or get_party_account_currency("Supplier", doc.get("supplier"), doc.get("company")),
				"buying_price_list": doc.get("buying_price_list")
				or frappe.db.get_single_value("Buying Settings", "buying_price_list"),
			}
		)
		add_items(sq_doc, doc.get("supplier"), doc.get("items"))
		sq_doc.flags.ignore_permissions = True
		sq_doc.run_method("set_missing_values")
		sq_doc.save()
		frappe.msgprint(_("Supplier Quotation {0} Created").format(sq_doc.name))
		return sq_doc.name
	except Exception:
		return None


def add_items(sq_doc, supplier, items):
	for data in items:
		if isinstance(data, dict):
			data = frappe._dict(data)

		create_rfq_items(sq_doc, supplier, data)


def create_rfq_items(sq_doc, supplier, data):
	args = {}

	for field in [
		"item_code",
		"item_name",
		"description",
		"qty",
		"rate",
		"conversion_factor",
		"warehouse",
		"material_request",
		"material_request_item",
		"stock_qty",
		"uom",
	]:
		args[field] = data.get(field)

	args.update(
		{
			"request_for_quotation_item": data.name,
			"request_for_quotation": data.parent,
			"supplier_part_no": frappe.db.get_value(
				"Item Supplier", {"parent": data.item_code, "supplier": supplier}, "supplier_part_no"
			),
		}
	)

	sq_doc.append("items", args)


@frappe.whitelist()
def get_pdf(
	name: str,
	supplier: str,
	print_format: str | None = None,
	language: str | None = None,
	letterhead: str | None = None,
):
	doc = frappe.get_doc("Request for Quotation", name)
	if supplier:
		doc.update_supplier_part_no(supplier)

	# permissions get checked in `download_pdf`
	download_pdf(
		doc.doctype,
		doc.name,
		print_format,
		doc=doc,
		language=language,
		letterhead=letterhead or None,
	)


@frappe.whitelist()
def get_item_from_material_requests_based_on_supplier(source_name, target_doc=None):
	mr_items_list = frappe.db.sql(
		"""
		SELECT
			mr.name, mr_item.item_code
		FROM
			`tabItem` as item,
			`tabItem Supplier` as item_supp,
			`tabMaterial Request Item` as mr_item,
			`tabMaterial Request`  as mr
		WHERE item_supp.supplier = %(supplier)s
			AND item.name = item_supp.parent
			AND mr_item.parent = mr.name
			AND mr_item.item_code = item.name
			AND mr.status != "Stopped"
			AND mr.material_request_type = "Purchase"
			AND mr.docstatus = 1
			AND mr.per_ordered < 99.99""",
		{"supplier": source_name},
		as_dict=1,
	)

	material_requests = {}
	for d in mr_items_list:
		material_requests.setdefault(d.name, []).append(d.item_code)

	for mr, items in material_requests.items():
		target_doc = get_mapped_doc(
			"Material Request",
			mr,
			{
				"Material Request": {
					"doctype": "Request for Quotation",
					"validation": {
						"docstatus": ["=", 1],
						"material_request_type": ["=", "Purchase"],
					},
				},
				"Material Request Item": {
					"doctype": "Request for Quotation Item",
					"condition": lambda row: row.item_code in items,
					"field_map": [
						["name", "material_request_item"],
						["parent", "material_request"],
						["uom", "uom"],
					],
				},
			},
			target_doc,
		)

	return target_doc


@frappe.whitelist()
def get_supplier_tag():
	filters = {"document_type": "Supplier"}
	tags = list(set(tag.tag for tag in frappe.get_all("Tag Link", filters=filters, fields=["tag"]) if tag))

	return tags


@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_rfq_containing_supplier(doctype, txt, searchfield, start, page_len, filters):
	rfq = frappe.qb.DocType("Request for Quotation")
	rfq_supplier = frappe.qb.DocType("Request for Quotation Supplier")

	query = (
		frappe.qb.from_(rfq)
		.from_(rfq_supplier)
		.select(rfq.name)
		.distinct()
		.select(rfq.transaction_date, rfq.company)
		.where(
			(rfq.name == rfq_supplier.parent)
			& (rfq_supplier.supplier == filters.get("supplier"))
			& (rfq.docstatus == 1)
			& (rfq.company == filters.get("company"))
		)
		.orderby(rfq.transaction_date, order=Order.asc)
		.limit(page_len)
		.offset(start)
	)

	if txt:
		query = query.where(rfq.name.like(f"%%{txt}%%"))

	if filters.get("transaction_date"):
		query = query.where(rfq.transaction_date == filters.get("transaction_date"))

	rfq_data = query.run(as_dict=1)

	return rfq_data
