package models

import (
	"encoding/xml"
	"fmt"
	"strings"
	"time"

	"odoo-go/pkg/orm"
)

// initAccountEdi registers electronic invoicing (EDI) models.
// Mirrors: odoo/addons/account_edi/models/account_edi_format.py
//
// EDI (Electronic Data Interchange) handles electronic invoice formats like
// UBL, Factur-X, and XRechnung. This provides the base framework.
func initAccountEdi() {
	// -- EDI Format --
	// Defines a supported electronic invoice format.
	ef := orm.NewModel("account.edi.format", orm.ModelOpts{
		Description: "Electronic Invoicing Format",
		Order:       "name",
	})

	ef.AddFields(
		orm.Char("name", orm.FieldOpts{String: "Name", Required: true}),
		orm.Char("code", orm.FieldOpts{String: "Code", Required: true, Help: "Technical code, e.g. facturx_1_0_05, ubl_2_1"}),
	)

	// -- EDI Document --
	// Links an invoice to an EDI format with state tracking.
	ed := orm.NewModel("account.edi.document", orm.ModelOpts{
		Description: "Electronic Invoicing Document",
		Order:       "id desc",
	})

	ed.AddFields(
		orm.Many2one("move_id", "account.move", orm.FieldOpts{
			String: "Invoice", Required: true, OnDelete: orm.OnDeleteCascade, Index: true,
		}),
		orm.Many2one("edi_format_id", "account.edi.format", orm.FieldOpts{
			String: "Format", Required: true,
		}),
		orm.Selection("state", []orm.SelectionItem{
			{Value: "to_send", Label: "To Send"},
			{Value: "sent", Label: "Sent"},
			{Value: "to_cancel", Label: "To Cancel"},
			{Value: "cancelled", Label: "Cancelled"},
		}, orm.FieldOpts{String: "Status", Default: "to_send", Required: true}),
		orm.Binary("attachment_id", orm.FieldOpts{String: "Attachment"}),
		orm.Char("error", orm.FieldOpts{String: "Error"}),
		orm.Boolean("blocking_level", orm.FieldOpts{String: "Blocking Level"}),
	)

	// action_export_xml: generate UBL XML for the invoice.
	// Mirrors: odoo/addons/account_edi/models/account_edi_format.py _export_invoice_ubl()
	ed.RegisterMethod("action_export_xml", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
		env := rs.Env()
		docID := rs.IDs()[0]

		var moveID int64
		var formatCode string
		env.Tx().QueryRow(env.Ctx(),
			`SELECT d.move_id, f.code
			 FROM account_edi_document d
			 JOIN account_edi_format f ON f.id = d.edi_format_id
			 WHERE d.id = $1`, docID,
		).Scan(&moveID, &formatCode)

		if moveID == 0 {
			return nil, fmt.Errorf("account: EDI document has no linked invoice")
		}

		xmlContent, err := generateInvoiceXML(env, moveID, formatCode)
		if err != nil {
			env.Tx().Exec(env.Ctx(),
				`UPDATE account_edi_document SET error = $1 WHERE id = $2`,
				err.Error(), docID)
			return nil, err
		}

		// Mark as sent
		env.Tx().Exec(env.Ctx(),
			`UPDATE account_edi_document SET state = 'sent', error = NULL WHERE id = $1`, docID)

		return map[string]interface{}{
			"xml":     xmlContent,
			"move_id": moveID,
			"format":  formatCode,
		}, nil
	})

	// Extend account.move with EDI fields
	initAccountMoveEdiExtension()
}

// initAccountMoveEdiExtension adds EDI-related fields to account.move.
func initAccountMoveEdiExtension() {
	ext := orm.ExtendModel("account.move")
	ext.AddFields(
		orm.One2many("edi_document_ids", "account.edi.document", "move_id", orm.FieldOpts{
			String: "Electronic Documents",
		}),
		orm.Selection("edi_state", []orm.SelectionItem{
			{Value: "to_send", Label: "To Send"},
			{Value: "sent", Label: "Sent"},
			{Value: "to_cancel", Label: "To Cancel"},
			{Value: "cancelled", Label: "Cancelled"},
		}, orm.FieldOpts{String: "Electronic Invoicing State"}),
		orm.Boolean("edi_show_cancel_button", orm.FieldOpts{
			String: "Show Cancel EDI Button", Compute: "_compute_edi_show_cancel_button",
		}),
	)

	// _compute_edi_show_cancel_button
	ext.RegisterCompute("edi_show_cancel_button", func(rs *orm.Recordset) (orm.Values, error) {
		env := rs.Env()
		moveID := rs.IDs()[0]

		var sentCount int
		env.Tx().QueryRow(env.Ctx(),
			`SELECT COUNT(*) FROM account_edi_document WHERE move_id = $1 AND state = 'sent'`, moveID,
		).Scan(&sentCount)

		return orm.Values{"edi_show_cancel_button": sentCount > 0}, nil
	})

	// action_process_edi_web_services: send all pending EDI documents
	ext.RegisterMethod("action_process_edi_web_services", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
		env := rs.Env()
		for _, moveID := range rs.IDs() {
			// Find pending EDI documents
			rows, err := env.Tx().Query(env.Ctx(),
				`SELECT id FROM account_edi_document WHERE move_id = $1 AND state = 'to_send'`, moveID)
			if err != nil {
				continue
			}
			var docIDs []int64
			for rows.Next() {
				var docID int64
				rows.Scan(&docID)
				docIDs = append(docIDs, docID)
			}
			rows.Close()

			for _, docID := range docIDs {
				docRS := env.Model("account.edi.document").Browse(docID)
				ediModel := orm.Registry.Get("account.edi.document")
				if ediModel != nil {
					if exportMethod, ok := ediModel.Methods["action_export_xml"]; ok {
						exportMethod(docRS)
					}
				}
			}
		}
		return true, nil
	})
}

// -- XML generation for UBL/Factur-X --

// UBLInvoice represents a simplified UBL 2.1 invoice structure.
type UBLInvoice struct {
	XMLName             xml.Name     `xml:"Invoice"`
	XMLNS               string       `xml:"xmlns,attr"`
	XMLNSCAC            string       `xml:"xmlns:cac,attr"`
	XMLNSCBC            string       `xml:"xmlns:cbc,attr"`
	UBLVersionID        string       `xml:"cbc:UBLVersionID"`
	CustomizationID     string       `xml:"cbc:CustomizationID"`
	ID                  string       `xml:"cbc:ID"`
	IssueDate           string       `xml:"cbc:IssueDate"`
	DueDate             string       `xml:"cbc:DueDate,omitempty"`
	InvoiceTypeCode     string       `xml:"cbc:InvoiceTypeCode"`
	DocumentCurrencyCode string      `xml:"cbc:DocumentCurrencyCode"`
	Supplier            UBLParty     `xml:"cac:AccountingSupplierParty>cac:Party"`
	Customer            UBLParty     `xml:"cac:AccountingCustomerParty>cac:Party"`
	TaxTotal            UBLTaxTotal  `xml:"cac:TaxTotal"`
	LegalMonetaryTotal  UBLMonetary  `xml:"cac:LegalMonetaryTotal"`
	InvoiceLines        []UBLLine    `xml:"cac:InvoiceLine"`
}

// UBLParty represents a party in UBL.
type UBLParty struct {
	Name    string `xml:"cac:PartyName>cbc:Name"`
	Street  string `xml:"cac:PostalAddress>cbc:StreetName,omitempty"`
	City    string `xml:"cac:PostalAddress>cbc:CityName,omitempty"`
	Zip     string `xml:"cac:PostalAddress>cbc:PostalZone,omitempty"`
	Country string `xml:"cac:PostalAddress>cac:Country>cbc:IdentificationCode,omitempty"`
	TaxID   string `xml:"cac:PartyTaxScheme>cbc:CompanyID,omitempty"`
}

// UBLTaxTotal represents tax totals.
type UBLTaxTotal struct {
	TaxAmount string `xml:"cbc:TaxAmount"`
}

// UBLMonetary represents monetary totals.
type UBLMonetary struct {
	LineExtensionAmount string `xml:"cbc:LineExtensionAmount"`
	TaxExclusiveAmount  string `xml:"cbc:TaxExclusiveAmount"`
	TaxInclusiveAmount  string `xml:"cbc:TaxInclusiveAmount"`
	PayableAmount       string `xml:"cbc:PayableAmount"`
}

// UBLLine represents an invoice line in UBL.
type UBLLine struct {
	ID           string `xml:"cbc:ID"`
	Quantity     string `xml:"cbc:InvoicedQuantity"`
	LineAmount   string `xml:"cbc:LineExtensionAmount"`
	ItemName     string `xml:"cac:Item>cbc:Name"`
	PriceAmount  string `xml:"cac:Price>cbc:PriceAmount"`
}

// generateInvoiceXML creates a UBL 2.1 XML representation of an invoice.
// Mirrors: odoo/addons/account_edi_ubl_cii/models/account_edi_xml_ubl_20.py
func generateInvoiceXML(env *orm.Environment, moveID int64, formatCode string) (string, error) {
	// Read move header
	var name, moveType string
	var invoiceDate, dueDate *string
	var partnerID, companyID int64
	var amountUntaxed, amountTax, amountTotal float64
	err := env.Tx().QueryRow(env.Ctx(),
		`SELECT COALESCE(name, '/'), COALESCE(move_type, 'out_invoice'),
		        invoice_date::text, invoice_date_due::text,
		        COALESCE(partner_id, 0), COALESCE(company_id, 0),
		        COALESCE(amount_untaxed::float8, 0), COALESCE(amount_tax::float8, 0),
		        COALESCE(amount_total::float8, 0)
		 FROM account_move WHERE id = $1`, moveID,
	).Scan(&name, &moveType, &invoiceDate, &dueDate, &partnerID, &companyID,
		&amountUntaxed, &amountTax, &amountTotal)
	if err != nil {
		return "", fmt.Errorf("account: read move for XML: %w", err)
	}

	// Determine invoice type code (UBL standard)
	typeCode := "380" // Commercial Invoice
	switch moveType {
	case "out_refund", "in_refund":
		typeCode = "381" // Credit Note
	case "out_receipt", "in_receipt":
		typeCode = "325" // Receipt
	}

	// Read supplier (company)
	var companyName string
	var companyStreet, companyCity, companyZip, companyCountry *string
	var companyVat *string
	env.Tx().QueryRow(env.Ctx(),
		`SELECT COALESCE(c.name, ''), p.street, p.city, p.zip,
		        co.code, c.vat
		 FROM res_company c
		 LEFT JOIN res_partner p ON p.id = c.partner_id
		 LEFT JOIN res_country co ON co.id = p.country_id
		 WHERE c.id = $1`, companyID,
	).Scan(&companyName, &companyStreet, &companyCity, &companyZip, &companyCountry, &companyVat)

	// Read customer (partner)
	var customerName string
	var customerStreet, customerCity, customerZip, customerCountry *string
	var customerVat *string
	env.Tx().QueryRow(env.Ctx(),
		`SELECT COALESCE(p.name, ''), p.street, p.city, p.zip,
		        co.code, p.vat
		 FROM res_partner p
		 LEFT JOIN res_country co ON co.id = p.country_id
		 WHERE p.id = $1`, partnerID,
	).Scan(&customerName, &customerStreet, &customerCity, &customerZip, &customerCountry, &customerVat)

	// Read invoice lines
	lineRows, err := env.Tx().Query(env.Ctx(),
		`SELECT COALESCE(name, ''), COALESCE(quantity, 1), COALESCE(price_unit::float8, 0),
		        COALESCE(price_subtotal::float8, 0)
		 FROM account_move_line
		 WHERE move_id = $1 AND (display_type IS NULL OR display_type = '' OR display_type = 'product')
		 ORDER BY sequence, id`, moveID)
	if err != nil {
		return "", fmt.Errorf("account: read lines for XML: %w", err)
	}
	defer lineRows.Close()

	var ublLines []UBLLine
	lineNum := 1
	for lineRows.Next() {
		var lineName string
		var qty, price, subtotal float64
		if err := lineRows.Scan(&lineName, &qty, &price, &subtotal); err != nil {
			continue
		}
		ublLines = append(ublLines, UBLLine{
			ID:          fmt.Sprintf("%d", lineNum),
			Quantity:    fmt.Sprintf("%.2f", qty),
			LineAmount:  fmt.Sprintf("%.2f", subtotal),
			ItemName:    lineName,
			PriceAmount: fmt.Sprintf("%.2f", price),
		})
		lineNum++
	}

	issueDateStr := time.Now().Format("2006-01-02")
	if invoiceDate != nil && *invoiceDate != "" {
		issueDateStr = *invoiceDate
	}
	dueDateStr := ""
	if dueDate != nil {
		dueDateStr = *dueDate
	}

	invoice := UBLInvoice{
		XMLNS:                "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2",
		XMLNSCAC:             "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2",
		XMLNSCBC:             "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2",
		UBLVersionID:         "2.1",
		CustomizationID:      "urn:cen.eu:en16931:2017",
		ID:                   name,
		IssueDate:            issueDateStr,
		DueDate:              dueDateStr,
		InvoiceTypeCode:      typeCode,
		DocumentCurrencyCode: "EUR",
		Supplier: UBLParty{
			Name:    companyName,
			Street:  ptrStr(companyStreet),
			City:    ptrStr(companyCity),
			Zip:     ptrStr(companyZip),
			Country: ptrStr(companyCountry),
			TaxID:   ptrStr(companyVat),
		},
		Customer: UBLParty{
			Name:    customerName,
			Street:  ptrStr(customerStreet),
			City:    ptrStr(customerCity),
			Zip:     ptrStr(customerZip),
			Country: ptrStr(customerCountry),
			TaxID:   ptrStr(customerVat),
		},
		TaxTotal: UBLTaxTotal{
			TaxAmount: fmt.Sprintf("%.2f", amountTax),
		},
		LegalMonetaryTotal: UBLMonetary{
			LineExtensionAmount: fmt.Sprintf("%.2f", amountUntaxed),
			TaxExclusiveAmount:  fmt.Sprintf("%.2f", amountUntaxed),
			TaxInclusiveAmount:  fmt.Sprintf("%.2f", amountTotal),
			PayableAmount:       fmt.Sprintf("%.2f", amountTotal),
		},
		InvoiceLines: ublLines,
	}

	output, err := xml.MarshalIndent(invoice, "", "  ")
	if err != nil {
		return "", fmt.Errorf("account: marshal XML: %w", err)
	}

	var b strings.Builder
	b.WriteString(xml.Header)
	b.Write(output)

	return b.String(), nil
}

// ptrStr safely dereferences a *string, returning "" if nil.
func ptrStr(s *string) string {
	if s != nil {
		return *s
	}
	return ""
}
