package ofxgo

import (
	"errors"
	"github.com/aclindsa/xml"
)

// SecurityID identifies a security by its CUSIP (for US-based FI's, others may
// use UniqueID types other than CUSIP)
type SecurityID struct {
	XMLName      xml.Name `xml:"SECID"`
	UniqueID     String   `xml:"UNIQUEID"`     // CUSIP for US FI's
	UniqueIDType String   `xml:"UNIQUEIDTYPE"` // Should always be "CUSIP" for US FI's
}

// SecurityRequest represents a request for one security. It is specified with
// a SECID aggregate, a ticker symbol, or an FI assigned identifier (but no
// more than one of them at a time)
type SecurityRequest struct {
	XMLName xml.Name `xml:"SECRQ"`
	// Only one of the next three should be present
	SecID  *SecurityID `xml:"SECID,omitempty"`
	Ticker String      `xml:"TICKER,omitempty"`
	FiID   String      `xml:"FIID,omitempty"`
}

// SecListRequest represents a request for information (namely price) about one
// or more securities
type SecListRequest struct {
	XMLName   xml.Name `xml:"SECLISTTRNRQ"`
	TrnUID    UID      `xml:"TRNUID"`
	CltCookie String   `xml:"CLTCOOKIE,omitempty"`
	TAN       String   `xml:"TAN,omitempty"` // Transaction authorization number
	// TODO `xml:"OFXEXTENSION,omitempty"`
	Securities []SecurityRequest `xml:"SECLISTRQ>SECRQ,omitempty"`
}

// Name returns the name of the top-level transaction XML/SGML element
func (r *SecListRequest) Name() string {
	return "SECLISTTRNRQ"
}

// Valid returns (true, nil) if this struct would be valid OFX if marshalled
// into XML/SGML
func (r *SecListRequest) Valid(version ofxVersion) (bool, error) {
	if ok, err := r.TrnUID.Valid(); !ok {
		return false, err
	}
	// TODO implement
	return true, nil
}

// Type returns which message set this message belongs to (which Request
// element of type []Message it should appended to)
func (r *SecListRequest) Type() messageType {
	return SecListRq
}

// SecListResponse is always empty (except for the transaction UID, status, and
// optional client cookie). Its presence signifies that the SecurityList (a
// different element from this one) immediately after this element in
// Response.SecList was been generated in response to the same SecListRequest
// this is a response to.
type SecListResponse struct {
	XMLName   xml.Name `xml:"SECLISTTRNRS"`
	TrnUID    UID      `xml:"TRNUID"`
	Status    Status   `xml:"STATUS"`
	CltCookie String   `xml:"CLTCOOKIE,omitempty"`
	// TODO `xml:"OFXEXTENSION,omitempty"`
	// SECLISTRS is always empty, so we don't parse it here. The actual securities list will be in a top-level element parallel to SECLISTTRNRS
}

// Name returns the name of the top-level transaction XML/SGML element
func (r *SecListResponse) Name() string {
	return "SECLISTTRNRS"
}

// Valid returns (true, nil) if this struct was valid OFX when unmarshalled
func (r *SecListResponse) Valid(version ofxVersion) (bool, error) {
	if ok, err := r.TrnUID.Valid(); !ok {
		return false, err
	}
	// TODO implement
	return true, nil
}

// Type returns which message set this message belongs to (which Response
// element of type []Message it belongs to)
func (r *SecListResponse) Type() messageType {
	return SecListRs
}

// Security is satisfied by all *Info elements providing information about
// securities for SecurityList
type Security interface {
	SecurityType() string
	SecurityInfo() SecInfo
}

// SecInfo represents the generic information about a security. It is included
// in most other *Info elements.
type SecInfo struct {
	XMLName   xml.Name   `xml:"SECINFO"`
	SecID     SecurityID `xml:"SECID"`
	SecName   String     `xml:"SECNAME"`          // Full name of security
	Ticker    String     `xml:"TICKER,omitempty"` // Ticker symbol
	FiID      String     `xml:"FIID,omitempty"`
	Rating    String     `xml:"RATING,omitempty"`
	UnitPrice Amount     `xml:"UNITPRICE,omitempty"` // Current price, as of DTASOF
	DtAsOf    *Date      `xml:"DTASOF,omitempty"`    // Date UNITPRICE was for
	Currency  *Currency  `xml:"CURRENCY,omitempty"`  // Overriding currency for UNITPRICE
	Memo      String     `xml:"MEMO,omitempty"`
}

// DebtInfo provides information about a debt security
type DebtInfo struct {
	XMLName      xml.Name   `xml:"DEBTINFO"`
	SecInfo      SecInfo    `xml:"SECINFO"`
	ParValue     Amount     `xml:"PARVALUE"`
	DebtType     debtType   `xml:"DEBTTYPE"`               // One of COUPON, ZERO (zero coupon)
	DebtClass    debtClass  `xml:"DEBTCLASS,omitempty"`    // One of TREASURY, MUNICIPAL, CORPORATE, OTHER
	CouponRate   Amount     `xml:"COUPONRT,omitempty"`     // Bond coupon rate for next closest call date
	DtCoupon     *Date      `xml:"DTCOUPON,omitempty"`     // Maturity date for next coupon
	CouponFreq   couponFreq `xml:"COUPONFREQ,omitempty"`   // When coupons mature - one of MONTHLY, QUARTERLY, SEMIANNUAL, ANNUAL, or OTHER
	CallPrice    Amount     `xml:"CALLPRICE,omitempty"`    // Bond call price
	YieldToCall  Amount     `xml:"YIELDTOCALL,omitempty"`  // Yield to next call
	DtCall       *Date      `xml:"DTCALL,omitempty"`       // Next call date
	CallType     callType   `xml:"CALLTYPE,omitempt"`      // Type of next call. One of CALL, PUT, PREFUND, MATURITY
	YieldToMat   Amount     `xml:"YIELDTOMAT,omitempty"`   // Yield to maturity
	DtMat        *Date      `xml:"DTMAT,omitempty"`        // Debt maturity date
	AssetClass   assetClass `xml:"ASSETCLASS,omitempty"`   // One of DOMESTICBOND, INTLBOND, LARGESTOCK, SMALLSTOCK, INTLSTOCK, MONEYMRKT, OTHER
	FiAssetClass String     `xml:"FIASSETCLASS,omitempty"` // FI-defined asset class
}

// SecurityType returns a string representation of this security's type
func (i DebtInfo) SecurityType() string {
	return "DEBTINFO"
}

// SecurityInfo returns SecInfo
func (i DebtInfo) SecurityInfo() SecInfo {
	return i.SecInfo
}

// AssetPortion represents the percentage of a mutual fund with the given asset
// classification
type AssetPortion struct {
	XMLName    xml.Name   `xml:"PORTION"`
	AssetClass assetClass `xml:"ASSETCLASS"` // One of DOMESTICBOND, INTLBOND, LARGESTOCK, SMALLSTOCK, INTLSTOCK, MONEYMRKT, OTHER
	Percent    Amount     `xml:"PERCENT"`    // Percentage of the fund that falls under this asset class
}

// FiAssetPortion represents the percentage of a mutual fund with the given
// FI-defined asset classification (AssetPortion should be used for all asset
// classifications defined by the assetClass enum)
type FiAssetPortion struct {
	XMLName      xml.Name `xml:"FIPORTION"`
	FiAssetClass String   `xml:"FIASSETCLASS,omitempty"` // FI-defined asset class
	Percent      Amount   `xml:"PERCENT"`                // Percentage of the fund that falls under this asset class
}

// MFInfo provides information about a mutual fund
type MFInfo struct {
	XMLName        xml.Name         `xml:"MFINFO"`
	SecInfo        SecInfo          `xml:"SECINFO"`
	MfType         mfType           `xml:"MFTYPE"`                // One of OPEN, END, CLOSEEND, OTHER
	Yield          Amount           `xml:"YIELD,omitempty"`       // Current yield reported as the dividend expressed as a portion of the current stock price
	DtYieldAsOf    *Date            `xml:"DTYIELDASOF,omitempty"` // Date YIELD is valid for
	AssetClasses   []AssetPortion   `xml:"MFASSETCLASS>PORTION"`
	FiAssetClasses []FiAssetPortion `xml:"FIMFASSETCLASS>FIPORTION"`
}

// SecurityType returns a string representation of this security's type
func (i MFInfo) SecurityType() string {
	return "MFINFO"
}

// SecurityInfo returns SecInfo
func (i MFInfo) SecurityInfo() SecInfo {
	return i.SecInfo
}

// OptInfo provides information about an option
type OptInfo struct {
	XMLName      xml.Name    `xml:"OPTINFO"`
	SecInfo      SecInfo     `xml:"SECINFO"`
	OptType      optType     `xml:"OPTTYPE"` // One of PUT, CALL
	StrikePrice  Amount      `xml:"STRIKEPRICE"`
	DtExpire     Date        `xml:"DTEXPIRE"`               // Expiration date
	ShPerCtrct   Int         `xml:"SHPERCTRCT"`             // Shares per contract
	SecID        *SecurityID `xml:"SECID,omitempty"`        // Security ID of the underlying security
	AssetClass   assetClass  `xml:"ASSETCLASS,omitempty"`   // One of DOMESTICBOND, INTLBOND, LARGESTOCK, SMALLSTOCK, INTLSTOCK, MONEYMRKT, OTHER
	FiAssetClass String      `xml:"FIASSETCLASS,omitempty"` // FI-defined asset class
}

// SecurityType returns a string representation of this security's type
func (i OptInfo) SecurityType() string {
	return "OPTINFO"
}

// SecurityInfo returns SecInfo
func (i OptInfo) SecurityInfo() SecInfo {
	return i.SecInfo
}

// OtherInfo provides information about a security type not covered by the
// other *Info elements
type OtherInfo struct {
	XMLName      xml.Name   `xml:"OTHERINFO"`
	SecInfo      SecInfo    `xml:"SECINFO"`
	TypeDesc     String     `xml:"TYPEDESC,omitempty"`     // Description of security type
	AssetClass   assetClass `xml:"ASSETCLASS,omitempty"`   // One of DOMESTICBOND, INTLBOND, LARGESTOCK, SMALLSTOCK, INTLSTOCK, MONEYMRKT, OTHER
	FiAssetClass String     `xml:"FIASSETCLASS,omitempty"` // FI-defined asset class
}

// SecurityType returns a string representation of this security's type
func (i OtherInfo) SecurityType() string {
	return "OTHERINFO"
}

// SecurityInfo returns SecInfo
func (i OtherInfo) SecurityInfo() SecInfo {
	return i.SecInfo
}

// StockInfo provides information about a security type
type StockInfo struct {
	XMLName      xml.Name   `xml:"STOCKINFO"`
	SecInfo      SecInfo    `xml:"SECINFO"`
	StockType    stockType  `xml:"STOCKTYPE,omitempty"`    // One of COMMON, PREFERRED, CONVERTIBLE, OTHER
	Yield        Amount     `xml:"YIELD,omitempty"`        // Current yield reported as the dividend expressed as a portion of the current stock price
	DtYieldAsOf  *Date      `xml:"DTYIELDASOF,omitempty"`  // Date YIELD is valid for
	AssetClass   assetClass `xml:"ASSETCLASS,omitempty"`   // One of DOMESTICBOND, INTLBOND, LARGESTOCK, SMALLSTOCK, INTLSTOCK, MONEYMRKT, OTHER
	FiAssetClass String     `xml:"FIASSETCLASS,omitempty"` // FI-defined asset class
}

// SecurityType returns a string representation of this security's type
func (i StockInfo) SecurityType() string {
	return "STOCKINFO"
}

// SecurityInfo returns SecInfo
func (i StockInfo) SecurityInfo() SecInfo {
	return i.SecInfo
}

// SecurityList is a container for Security objects containaing information
// about securities
type SecurityList struct {
	XMLName    xml.Name `xml:"SECLIST"`
	Securities []Security
}

// Name returns the name of the top-level transaction XML/SGML element
func (r *SecurityList) Name() string {
	return "SECLIST"
}

// Valid returns (true, nil) if this struct was valid OFX when unmarshalled
func (r *SecurityList) Valid(version ofxVersion) (bool, error) {
	// TODO implement
	return true, nil
}

// Type returns which message set this message belongs to (which Response
// element of type []Message it belongs to)
func (r *SecurityList) Type() messageType {
	return SecListRs
}

// UnmarshalXML handles unmarshalling a SecurityList from an SGML/XML string
func (r *SecurityList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
	for {
		tok, err := nextNonWhitespaceToken(d)
		if err != nil {
			return err
		} else if end, ok := tok.(xml.EndElement); ok && end.Name.Local == start.Name.Local {
			// If we found the end of our starting element, we're done parsing
			return nil
		} else if startElement, ok := tok.(xml.StartElement); ok {
			switch startElement.Name.Local {
			case "DEBTINFO":
				var info DebtInfo
				if err := d.DecodeElement(&info, &startElement); err != nil {
					return err
				}
				r.Securities = append(r.Securities, Security(info))
			case "MFINFO":
				var info MFInfo
				if err := d.DecodeElement(&info, &startElement); err != nil {
					return err
				}
				r.Securities = append(r.Securities, Security(info))
			case "OPTINFO":
				var info OptInfo
				if err := d.DecodeElement(&info, &startElement); err != nil {
					return err
				}
				r.Securities = append(r.Securities, Security(info))
			case "OTHERINFO":
				var info OtherInfo
				if err := d.DecodeElement(&info, &startElement); err != nil {
					return err
				}
				r.Securities = append(r.Securities, Security(info))
			case "STOCKINFO":
				var info StockInfo
				if err := d.DecodeElement(&info, &startElement); err != nil {
					return err
				}
				r.Securities = append(r.Securities, Security(info))
			default:
				return errors.New("Invalid SECLIST child tag: " + startElement.Name.Local)
			}
		} else {
			return errors.New("Didn't find an opening element")
		}
	}
}

// MarshalXML handles marshalling a SecurityList to an SGML/XML string
func (r *SecurityList) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
	secListElement := xml.StartElement{Name: xml.Name{Local: "SECLIST"}}
	if err := e.EncodeToken(secListElement); err != nil {
		return err
	}
	for _, s := range r.Securities {
		start := xml.StartElement{Name: xml.Name{Local: s.SecurityType()}}
		switch sec := s.(type) {
		case DebtInfo:
			if err := e.EncodeElement(&sec, start); err != nil {
				return err
			}
		case MFInfo:
			if err := e.EncodeElement(&sec, start); err != nil {
				return err
			}
		case OptInfo:
			if err := e.EncodeElement(&sec, start); err != nil {
				return err
			}
		case OtherInfo:
			if err := e.EncodeElement(&sec, start); err != nil {
				return err
			}
		case StockInfo:
			if err := e.EncodeElement(&sec, start); err != nil {
				return err
			}
		default:
			return errors.New("Invalid SECLIST child type: " + sec.SecurityType())
		}
	}
	if err := e.EncodeToken(secListElement.End()); err != nil {
		return err
	}
	return nil
}
