// Package kis decodes and normalizes Korea Investment & Securities (KIS) daily
// chart provider payloads into ALT domain market types, with live REST client
// wiring kept behind explicit runtime configuration.
package kis

import (
	"encoding/json"
	"fmt"
	"time"
	// tzdata is embedded so overseas venue timezones (e.g. America/New_York)
	// resolve deterministically without depending on host zoneinfo being present.
	_ "time/tzdata"

	"git.toki-labs.com/toki/alt/packages/domain/market"
)

const (
	DailyItemChartPricePath = "/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice"
	DailyItemChartPriceTRID = "FHKST03010100"

	// OverseasDailyPricePath and OverseasDailyPriceTRID address the KIS overseas
	// 기간별시세 (period price) endpoint used for individual US equities/ETFs. The
	// inquire-daily-chartprice endpoint only covers index constituents, so the
	// dailyprice endpoint is the one that resolves AAPL/SPY-style symbols.
	OverseasDailyPricePath = "/uapi/overseas-price/v1/quotations/dailyprice"
	OverseasDailyPriceTRID = "HHDFS76240000"
)

// Capability declares the KIS provider's daily bar coverage. provider != market:
// KIS serves both the KR domestic venue (KRX) and the US venues (NASDAQ, NYSE),
// so the capability lists venues across markets rather than pinning one market.
func Capability() market.ProviderCapability {
	return market.ProviderCapability{
		Provider: market.ProviderKIS,
		Venues: map[market.Venue]bool{
			market.VenueKRX:    true,
			market.VenueNASDAQ: true,
			market.VenueNYSE:   true,
		},
		Timeframes: map[market.Timeframe]bool{
			market.TimeframeDaily: true,
		},
	}
}

// seoulOffset is the fixed KST offset. KRX has no DST, so a fixed +09:00 zone is
// deterministic and avoids depending on tzdata being present at runtime.
const seoulOffsetSeconds = 9 * 60 * 60

// kisDateLayout matches the KIS stck_bsop_date field, e.g. "20240527".
const kisDateLayout = "20060102"

// DailyItemChartPriceRequest mirrors the KIS inquire-daily-itemchartprice
// request shape captured in the worker test fixtures.
type DailyItemChartPriceRequest struct {
	Provider string            `json:"provider"`
	Endpoint string            `json:"endpoint"`
	TrID     string            `json:"tr_id"`
	Params   map[string]string `json:"params"`
}

// DailyItemChartPriceResponse mirrors the KIS daily chart response payload.
type DailyItemChartPriceResponse struct {
	ReturnCode string                      `json:"rt_cd"`
	MessageCd  string                      `json:"msg_cd"`
	Message    string                      `json:"msg1"`
	Output1    DailyItemChartPriceSummary  `json:"output1"`
	Output2    []DailyItemChartPriceBarRow `json:"output2"`
}

// DailyItemChartPriceSummary is the KIS output1 instrument-level metadata block.
type DailyItemChartPriceSummary struct {
	Name          string `json:"hts_kor_isnm"`
	ShortCode     string `json:"stck_shrn_iscd"`
	CurrentPrice  string `json:"stck_prpr"`
	AccumVolume   string `json:"acml_vol"`
	AccumTradeAmt string `json:"acml_tr_pbmn"`
}

// DailyItemChartPriceBarRow is a single KIS output2 OHLCV row.
type DailyItemChartPriceBarRow struct {
	BusinessDate string `json:"stck_bsop_date"`
	Open         string `json:"stck_oprc"`
	High         string `json:"stck_hgpr"`
	Low          string `json:"stck_lwpr"`
	Close        string `json:"stck_clpr"`
	Volume       string `json:"acml_vol"`
	TradeAmount  string `json:"acml_tr_pbmn"`
}

// DecodeDailyItemChartPriceRequest decodes a KIS daily chart request fixture.
func DecodeDailyItemChartPriceRequest(data []byte) (DailyItemChartPriceRequest, error) {
	var req DailyItemChartPriceRequest
	if err := json.Unmarshal(data, &req); err != nil {
		return DailyItemChartPriceRequest{}, fmt.Errorf("decode kis daily request: %w", err)
	}
	return req, nil
}

// DecodeDailyItemChartPriceResponse decodes a KIS daily chart response fixture
// and validates that it represents a successful payload with bar rows.
func DecodeDailyItemChartPriceResponse(data []byte) (DailyItemChartPriceResponse, error) {
	var resp DailyItemChartPriceResponse
	if err := json.Unmarshal(data, &resp); err != nil {
		return DailyItemChartPriceResponse{}, fmt.Errorf("decode kis daily response: %w", err)
	}
	if resp.ReturnCode != "0" {
		return DailyItemChartPriceResponse{}, fmt.Errorf("kis daily response not successful: rt_cd=%q msg=%q", resp.ReturnCode, resp.Message)
	}
	if len(resp.Output2) == 0 {
		return DailyItemChartPriceResponse{}, fmt.Errorf("kis daily response has no output2 rows")
	}
	return resp, nil
}

// NormalizeDailyBars converts KIS output2 rows into ALT domain bars for the
// given instrument. KRX daily defaults are applied: Asia/Seoul midnight
// timestamps, the daily timeframe, and the instrument currency (KRW for KRX).
func NormalizeDailyBars(resp DailyItemChartPriceResponse, inst market.Instrument) ([]market.Bar, error) {
	currency := inst.Currency
	if currency == "" {
		currency = market.CurrencyKRW
	}
	seoul := time.FixedZone("Asia/Seoul", seoulOffsetSeconds)

	bars := make([]market.Bar, 0, len(resp.Output2))
	for i, row := range resp.Output2 {
		ts, err := time.ParseInLocation(kisDateLayout, row.BusinessDate, seoul)
		if err != nil {
			return nil, fmt.Errorf("parse business date for row %d: %w", i, err)
		}
		bars = append(bars, market.Bar{
			InstrumentID: inst.ID,
			Timeframe:    market.TimeframeDaily,
			Timestamp:    ts,
			Open:         market.Price{Currency: currency, Amount: market.Decimal{Value: row.Open}},
			High:         market.Price{Currency: currency, Amount: market.Decimal{Value: row.High}},
			Low:          market.Price{Currency: currency, Amount: market.Decimal{Value: row.Low}},
			Close:        market.Price{Currency: currency, Amount: market.Decimal{Value: row.Close}},
			Volume:       market.Quantity{Amount: market.Decimal{Value: row.Volume}},
		})
	}
	return bars, nil
}

// OverseasDailyPriceRequest mirrors the KIS overseas dailyprice request shape
// captured in the worker test fixtures.
type OverseasDailyPriceRequest struct {
	Provider string            `json:"provider"`
	Endpoint string            `json:"endpoint"`
	TrID     string            `json:"tr_id"`
	Params   map[string]string `json:"params"`
}

// OverseasDailyPriceResponse mirrors the KIS overseas dailyprice response. The
// overseas payload reuses the output1/output2 envelope but renames the OHLCV
// fields relative to the domestic chart response.
type OverseasDailyPriceResponse struct {
	ReturnCode string                     `json:"rt_cd"`
	MessageCd  string                     `json:"msg_cd"`
	Message    string                     `json:"msg1"`
	Output1    OverseasDailyPriceSummary  `json:"output1"`
	Output2    []OverseasDailyPriceBarRow `json:"output2"`
}

// OverseasDailyPriceSummary is the KIS overseas output1 metadata block.
type OverseasDailyPriceSummary struct {
	RealtimeSymbol string `json:"rsym"`
	DecimalDigits  string `json:"zdiv"`
	PrevClose      string `json:"nrec"`
}

// OverseasDailyPriceBarRow is a single KIS overseas output2 OHLCV row. The
// overseas date field is xymd and the close field is clos, unlike the domestic
// stck_bsop_date / stck_clpr names.
type OverseasDailyPriceBarRow struct {
	BusinessDate string `json:"xymd"`
	Open         string `json:"open"`
	High         string `json:"high"`
	Low          string `json:"low"`
	Close        string `json:"clos"`
	Volume       string `json:"tvol"`
	TradeAmount  string `json:"tamt"`
}

// DecodeOverseasDailyPriceRequest decodes a KIS overseas dailyprice request
// fixture.
func DecodeOverseasDailyPriceRequest(data []byte) (OverseasDailyPriceRequest, error) {
	var req OverseasDailyPriceRequest
	if err := json.Unmarshal(data, &req); err != nil {
		return OverseasDailyPriceRequest{}, fmt.Errorf("decode kis overseas daily request: %w", err)
	}
	return req, nil
}

// DecodeOverseasDailyPriceResponse decodes a KIS overseas dailyprice response
// fixture and validates that it represents a successful payload with bar rows.
func DecodeOverseasDailyPriceResponse(data []byte) (OverseasDailyPriceResponse, error) {
	var resp OverseasDailyPriceResponse
	if err := json.Unmarshal(data, &resp); err != nil {
		return OverseasDailyPriceResponse{}, fmt.Errorf("decode kis overseas daily response: %w", err)
	}
	if resp.ReturnCode != "0" {
		return OverseasDailyPriceResponse{}, fmt.Errorf("kis overseas daily response not successful: rt_cd=%q msg=%q", resp.ReturnCode, resp.Message)
	}
	if len(resp.Output2) == 0 {
		return OverseasDailyPriceResponse{}, fmt.Errorf("kis overseas daily response has no output2 rows")
	}
	return resp, nil
}

// NormalizeOverseasDailyBars converts KIS overseas output2 rows into ALT domain
// bars for the given instrument. Unlike the domestic normalizer it takes the
// timestamp timezone from the instrument's venue metadata (e.g.
// America/New_York for NASDAQ/NYSE) and the price currency from the instrument
// currency, so US bars land as USD bars at the venue's local midnight instead
// of inheriting the KRX/KRW domestic defaults.
func NormalizeOverseasDailyBars(resp OverseasDailyPriceResponse, inst market.Instrument) ([]market.Bar, error) {
	currency := inst.Currency
	loc, err := venueLocation(inst.Venue)
	if err != nil {
		return nil, err
	}

	bars := make([]market.Bar, 0, len(resp.Output2))
	for i, row := range resp.Output2 {
		ts, err := time.ParseInLocation(kisDateLayout, row.BusinessDate, loc)
		if err != nil {
			return nil, fmt.Errorf("parse overseas business date for row %d: %w", i, err)
		}
		bars = append(bars, market.Bar{
			InstrumentID: inst.ID,
			Timeframe:    market.TimeframeDaily,
			Timestamp:    ts,
			Open:         market.Price{Currency: currency, Amount: market.Decimal{Value: row.Open}},
			High:         market.Price{Currency: currency, Amount: market.Decimal{Value: row.High}},
			Low:          market.Price{Currency: currency, Amount: market.Decimal{Value: row.Low}},
			Close:        market.Price{Currency: currency, Amount: market.Decimal{Value: row.Close}},
			Volume:       market.Quantity{Amount: market.Decimal{Value: row.Volume}},
		})
	}
	return bars, nil
}

// venueLocation resolves the time zone a venue's daily bars are stamped in. KRX
// uses a fixed +09:00 zone (no DST) to stay independent of tzdata; other venues
// load their IANA timezone from the domain venue metadata.
func venueLocation(v market.Venue) (*time.Location, error) {
	meta, ok := market.GetVenueMetadata(v)
	if !ok {
		return nil, fmt.Errorf("unknown venue %q", v)
	}
	if meta.Venue == market.VenueKRX {
		return time.FixedZone("Asia/Seoul", seoulOffsetSeconds), nil
	}
	loc, err := time.LoadLocation(meta.Timezone)
	if err != nil {
		return nil, fmt.Errorf("load timezone %q for venue %q: %w", meta.Timezone, v, err)
	}
	return loc, nil
}
