/*
Real-time Online/Offline Charging System (OCS) for Telecom & ISP environments
Copyright (C) ITsysCOM GmbH

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>
*/

package engine

import (
	"encoding/csv"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/url"
	"os"
	"path"
	"path/filepath"
	"strings"

	"github.com/cgrates/cgrates/config"
	"github.com/cgrates/cgrates/utils"
	"golang.org/x/net/context"
	"golang.org/x/oauth2"
	"golang.org/x/oauth2/google"
	"google.golang.org/api/option"
	"google.golang.org/api/sheets/v4"
)

// CSVStorage the basic csv storage
type CSVStorage struct {
	sep       rune
	generator func() csvReaderCloser
	// file names
	destinationsFn           []string
	ratesFn                  []string
	destinationratesFn       []string
	timingsFn                []string
	destinationratetimingsFn []string
	ratingprofilesFn         []string
	sharedgroupsFn           []string
	actionsFn                []string
	actiontimingsFn          []string
	actiontriggersFn         []string
	accountactionsFn         []string
	resProfilesFn            []string
	ipProfilesFn             []string
	statsFn                  []string
	trendsFn                 []string
	rankingsFn               []string
	thresholdsFn             []string
	filterFn                 []string
	routeProfilesFn          []string
	attributeProfilesFn      []string
	chargerProfilesFn        []string
	dispatcherProfilesFn     []string
	dispatcherHostsFn        []string
}

// NewCSVStorage creates a CSV storage that takes the data from the paths specified
func NewCSVStorage(sep rune,
	destinationsFn, timingsFn, ratesFn, destinationratesFn,
	destinationratetimingsFn, ratingprofilesFn, sharedgroupsFn,
	actionsFn, actiontimingsFn, actiontriggersFn, accountactionsFn,
	resProfilesFn, ipProfilesFn, statsFn, trendsFn, rankingsFn, thresholdsFn, filterFn, routeProfilesFn,
	attributeProfilesFn, chargerProfilesFn, dispatcherProfilesFn, dispatcherHostsFn []string) *CSVStorage {
	return &CSVStorage{
		sep:                      sep,
		generator:                NewCsvFile,
		destinationsFn:           destinationsFn,
		timingsFn:                timingsFn,
		ratesFn:                  ratesFn,
		destinationratesFn:       destinationratesFn,
		destinationratetimingsFn: destinationratetimingsFn,
		ratingprofilesFn:         ratingprofilesFn,
		sharedgroupsFn:           sharedgroupsFn,
		actionsFn:                actionsFn,
		actiontimingsFn:          actiontimingsFn,
		actiontriggersFn:         actiontriggersFn,
		accountactionsFn:         accountactionsFn,
		resProfilesFn:            resProfilesFn,
		ipProfilesFn:             ipProfilesFn,
		statsFn:                  statsFn,
		trendsFn:                 trendsFn,
		rankingsFn:               rankingsFn,
		thresholdsFn:             thresholdsFn,
		filterFn:                 filterFn,
		routeProfilesFn:          routeProfilesFn,
		attributeProfilesFn:      attributeProfilesFn,
		chargerProfilesFn:        chargerProfilesFn,
		dispatcherProfilesFn:     dispatcherProfilesFn,
		dispatcherHostsFn:        dispatcherHostsFn,
	}
}

// NewFileCSVStorage returns a csv storage that uses all files from the folder
func NewFileCSVStorage(sep rune, dataPath string) (*CSVStorage, error) {
	allFoldersPath, err := getAllFolders(dataPath)
	if err != nil {
		return nil, fmt.Errorf("failed to retrieve the folders from data path '%s': %s", dataPath, err.Error())
	}
	destinationsPaths := appendName(allFoldersPath, utils.DestinationsCsv)
	timingsPaths := appendName(allFoldersPath, utils.TimingsCsv)
	ratesPaths := appendName(allFoldersPath, utils.RatesCsv)
	destinationRatesPaths := appendName(allFoldersPath, utils.DestinationRatesCsv)
	ratingPlansPaths := appendName(allFoldersPath, utils.RatingPlansCsv)
	ratingProfilesPaths := appendName(allFoldersPath, utils.RatingProfilesCsv)
	sharedGroupsPaths := appendName(allFoldersPath, utils.SharedGroupsCsv)
	actionsPaths := appendName(allFoldersPath, utils.ActionsCsv)
	actionPlansPaths := appendName(allFoldersPath, utils.ActionPlansCsv)
	actionTriggersPaths := appendName(allFoldersPath, utils.ActionTriggersCsv)
	accountActionsPaths := appendName(allFoldersPath, utils.AccountActionsCsv)
	resourcesPaths := appendName(allFoldersPath, utils.ResourcesCsv)
	ipsPaths := appendName(allFoldersPath, utils.IPsCsv)
	statsPaths := appendName(allFoldersPath, utils.StatsCsv)
	trendsPaths := appendName(allFoldersPath, utils.TrendsCsv)
	rankingsPaths := appendName(allFoldersPath, utils.RankingsCsv)
	thresholdsPaths := appendName(allFoldersPath, utils.ThresholdsCsv)
	filtersPaths := appendName(allFoldersPath, utils.FiltersCsv)
	routesPaths := appendName(allFoldersPath, utils.RoutesCsv)
	attributesPaths := appendName(allFoldersPath, utils.AttributesCsv)
	chargersPaths := appendName(allFoldersPath, utils.ChargersCsv)
	dispatcherprofilesPaths := appendName(allFoldersPath, utils.DispatcherProfilesCsv)
	dispatcherhostsPaths := appendName(allFoldersPath, utils.DispatcherHostsCsv)
	return NewCSVStorage(sep,
		destinationsPaths,
		timingsPaths,
		ratesPaths,
		destinationRatesPaths,
		ratingPlansPaths,
		ratingProfilesPaths,
		sharedGroupsPaths,
		actionsPaths,
		actionPlansPaths,
		actionTriggersPaths,
		accountActionsPaths,
		resourcesPaths,
		ipsPaths,
		statsPaths,
		trendsPaths,
		rankingsPaths,
		thresholdsPaths,
		filtersPaths,
		routesPaths,
		attributesPaths,
		chargersPaths,
		dispatcherprofilesPaths,
		dispatcherhostsPaths,
	), nil
}

// NewStringCSVStorage creates a csv storage from strings
func NewStringCSVStorage(sep rune,
	destinationsFn, timingsFn, ratesFn, destinationratesFn,
	destinationratetimingsFn, ratingprofilesFn, sharedgroupsFn,
	actionsFn, actiontimingsFn, actiontriggersFn, accountactionsFn,
	resProfilesFn, ipProfilesFn, statsFn, trendsFn, rankingsFn, thresholdsFn, filterFn, routeProfilesFn,
	attributeProfilesFn, chargerProfilesFn, dispatcherProfilesFn, dispatcherHostsFn string) *CSVStorage {
	c := NewCSVStorage(sep, []string{destinationsFn}, []string{timingsFn},
		[]string{ratesFn}, []string{destinationratesFn}, []string{destinationratetimingsFn},
		[]string{ratingprofilesFn}, []string{sharedgroupsFn}, []string{actionsFn},
		[]string{actiontimingsFn}, []string{actiontriggersFn}, []string{accountactionsFn},
		[]string{resProfilesFn}, []string{ipProfilesFn}, []string{statsFn}, []string{trendsFn}, []string{rankingsFn}, []string{thresholdsFn}, []string{filterFn},
		[]string{routeProfilesFn}, []string{attributeProfilesFn}, []string{chargerProfilesFn},
		[]string{dispatcherProfilesFn}, []string{dispatcherHostsFn})
	c.generator = NewCsvString
	return c
}

// NewGoogleCSVStorage creates a csv storege from google sheets
func NewGoogleCSVStorage(sep rune, spreadsheetID string) (*CSVStorage, error) {
	sht, err := newSheet()
	if err != nil {
		return nil, err
	}
	sheetNames, err := getSpreatsheetTabs(spreadsheetID, sht)
	if err != nil {
		return nil, err
	}
	getIfExist := func(name string) []string {
		if sheetNames.Has(name) {
			return []string{name}
		}
		return []string{}
	}
	c := NewCSVStorage(sep,
		getIfExist(utils.Destinations),
		getIfExist(utils.Timings),
		getIfExist(utils.Rates),
		getIfExist(utils.DestinationRates),
		getIfExist(utils.RatingPlans),
		getIfExist(utils.RatingProfiles),
		getIfExist(utils.SharedGroups),
		getIfExist(utils.Actions),
		getIfExist(utils.ActionPlans),
		getIfExist(utils.ActionTriggers),
		getIfExist(utils.AccountActions),
		getIfExist(utils.Resources),
		getIfExist(utils.IPs),
		getIfExist(utils.Stats),
		getIfExist(utils.Trends),
		getIfExist(utils.Trends),
		getIfExist(utils.Thresholds),
		getIfExist(utils.Filters),
		getIfExist(utils.Routes),
		getIfExist(utils.Attributes),
		getIfExist(utils.Chargers),
		getIfExist(utils.DispatcherProfiles),
		getIfExist(utils.DispatcherHosts),
	)
	c.generator = func() csvReaderCloser {
		return &csvGoogle{
			spreadsheetID: spreadsheetID,
			srv:           sht,
		}
	}
	return c, nil
}

// NewURLCSVStorage returns a CSVStorage that can parse URLs
func NewURLCSVStorage(sep rune, dataPath string) *CSVStorage {
	var destinationsPaths []string
	var timingsPaths []string
	var ratesPaths []string
	var destinationRatesPaths []string
	var ratingPlansPaths []string
	var ratingProfilesPaths []string
	var sharedGroupsPaths []string
	var actionsPaths []string
	var actionPlansPaths []string
	var actionTriggersPaths []string
	var accountActionsPaths []string
	var resourcesPaths []string
	var ipsPaths []string
	var statsPaths []string
	var trendsPaths []string
	var rankingsPaths []string
	var thresholdsPaths []string
	var filtersPaths []string
	var routesPaths []string
	var attributesPaths []string
	var chargersPaths []string
	var dispatcherprofilesPaths []string
	var dispatcherhostsPaths []string

	for _, baseURL := range strings.Split(dataPath, utils.InfieldSep) {
		if !strings.HasSuffix(baseURL, utils.CSVSuffix) {
			destinationsPaths = append(destinationsPaths, joinURL(baseURL, utils.DestinationsCsv))
			timingsPaths = append(timingsPaths, joinURL(baseURL, utils.TimingsCsv))
			ratesPaths = append(ratesPaths, joinURL(baseURL, utils.RatesCsv))
			destinationRatesPaths = append(destinationRatesPaths, joinURL(baseURL, utils.DestinationRatesCsv))
			ratingPlansPaths = append(ratingPlansPaths, joinURL(baseURL, utils.RatingPlansCsv))
			ratingProfilesPaths = append(ratingProfilesPaths, joinURL(baseURL, utils.RatingProfilesCsv))
			sharedGroupsPaths = append(sharedGroupsPaths, joinURL(baseURL, utils.SharedGroupsCsv))
			actionsPaths = append(actionsPaths, joinURL(baseURL, utils.ActionsCsv))
			actionPlansPaths = append(actionPlansPaths, joinURL(baseURL, utils.ActionPlansCsv))
			actionTriggersPaths = append(actionTriggersPaths, joinURL(baseURL, utils.ActionTriggersCsv))
			accountActionsPaths = append(accountActionsPaths, joinURL(baseURL, utils.AccountActionsCsv))
			resourcesPaths = append(resourcesPaths, joinURL(baseURL, utils.ResourcesCsv))
			ipsPaths = append(ipsPaths, joinURL(baseURL, utils.IPsCsv))
			statsPaths = append(statsPaths, joinURL(baseURL, utils.StatsCsv))
			trendsPaths = append(trendsPaths, joinURL(baseURL, utils.TrendsCsv))
			rankingsPaths = append(rankingsPaths, joinURL(baseURL, utils.RankingsCsv))
			thresholdsPaths = append(thresholdsPaths, joinURL(baseURL, utils.ThresholdsCsv))
			filtersPaths = append(filtersPaths, joinURL(baseURL, utils.FiltersCsv))
			routesPaths = append(routesPaths, joinURL(baseURL, utils.RoutesCsv))
			attributesPaths = append(attributesPaths, joinURL(baseURL, utils.AttributesCsv))
			chargersPaths = append(chargersPaths, joinURL(baseURL, utils.ChargersCsv))
			dispatcherprofilesPaths = append(dispatcherprofilesPaths, joinURL(baseURL, utils.DispatcherProfilesCsv))
			dispatcherhostsPaths = append(dispatcherhostsPaths, joinURL(baseURL, utils.DispatcherHostsCsv))
			continue
		}
		switch {
		case strings.HasSuffix(baseURL, utils.DestinationsCsv):
			destinationsPaths = append(destinationsPaths, baseURL)
		case strings.HasSuffix(baseURL, utils.TimingsCsv):
			timingsPaths = append(timingsPaths, baseURL)
		case strings.HasSuffix(baseURL, utils.RatesCsv):
			ratesPaths = append(ratesPaths, baseURL)
		case strings.HasSuffix(baseURL, utils.DestinationRatesCsv):
			destinationRatesPaths = append(destinationRatesPaths, baseURL)
		case strings.HasSuffix(baseURL, utils.RatingPlansCsv):
			ratingPlansPaths = append(ratingPlansPaths, baseURL)
		case strings.HasSuffix(baseURL, utils.RatingProfilesCsv):
			ratingProfilesPaths = append(ratingProfilesPaths, baseURL)
		case strings.HasSuffix(baseURL, utils.SharedGroupsCsv):
			sharedGroupsPaths = append(sharedGroupsPaths, baseURL)
		case strings.HasSuffix(baseURL, utils.ActionsCsv):
			actionsPaths = append(actionsPaths, baseURL)
		case strings.HasSuffix(baseURL, utils.ActionPlansCsv):
			actionPlansPaths = append(actionPlansPaths, baseURL)
		case strings.HasSuffix(baseURL, utils.ActionTriggersCsv):
			actionTriggersPaths = append(actionTriggersPaths, baseURL)
		case strings.HasSuffix(baseURL, utils.AccountActionsCsv):
			accountActionsPaths = append(accountActionsPaths, baseURL)
		case strings.HasSuffix(baseURL, utils.ResourcesCsv):
			resourcesPaths = append(resourcesPaths, baseURL)
		case strings.HasSuffix(baseURL, utils.IPsCsv):
			ipsPaths = append(ipsPaths, baseURL)
		case strings.HasSuffix(baseURL, utils.StatsCsv):
			statsPaths = append(statsPaths, baseURL)
		case strings.HasSuffix(baseURL, utils.TrendsCsv):
			trendsPaths = append(trendsPaths, baseURL)
		case strings.HasSuffix(baseURL, utils.RankingsCsv):
			rankingsPaths = append(rankingsPaths, baseURL)
		case strings.HasSuffix(baseURL, utils.ThresholdsCsv):
			thresholdsPaths = append(thresholdsPaths, baseURL)
		case strings.HasSuffix(baseURL, utils.FiltersCsv):
			filtersPaths = append(filtersPaths, baseURL)
		case strings.HasSuffix(baseURL, utils.RoutesCsv):
			routesPaths = append(routesPaths, baseURL)
		case strings.HasSuffix(baseURL, utils.AttributesCsv):
			attributesPaths = append(attributesPaths, baseURL)
		case strings.HasSuffix(baseURL, utils.ChargersCsv):
			chargersPaths = append(chargersPaths, baseURL)
		case strings.HasSuffix(baseURL, utils.DispatcherProfilesCsv):
			dispatcherprofilesPaths = append(dispatcherprofilesPaths, baseURL)
		case strings.HasSuffix(baseURL, utils.DispatcherHostsCsv):
			dispatcherhostsPaths = append(dispatcherhostsPaths, baseURL)
		}
	}

	c := NewCSVStorage(sep,
		destinationsPaths,
		timingsPaths,
		ratesPaths,
		destinationRatesPaths,
		ratingPlansPaths,
		ratingProfilesPaths,
		sharedGroupsPaths,
		actionsPaths,
		actionPlansPaths,
		actionTriggersPaths,
		accountActionsPaths,
		resourcesPaths,
		ipsPaths,
		statsPaths,
		trendsPaths,
		rankingsPaths,
		thresholdsPaths,
		filtersPaths,
		routesPaths,
		attributesPaths,
		chargersPaths,
		dispatcherprofilesPaths,
		dispatcherhostsPaths,
	)
	c.generator = func() csvReaderCloser {
		return &csvURL{}
	}
	return c
}

func joinURL(baseURL, fn string) string {
	u, err := url.Parse(baseURL)
	if err != nil {
		return baseURL + fn
	}
	u.Path = path.Join(u.Path, fn)
	return u.String()
}

func getAllFolders(inPath string) (paths []string, err error) {
	err = filepath.Walk(inPath, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if info.IsDir() {
			paths = append(paths, path)
		}
		return nil
	})
	return
}

func appendName(paths []string, fileName string) (out []string) {
	out = make([]string, len(paths))
	for i, basePath := range paths {
		out[i] = path.Join(basePath, fileName)
	}
	return
}

func (csvs *CSVStorage) proccesData(listType any, fns []string, process func(any)) error {
	collumnCount := getColumnCount(listType)
	for _, fileName := range fns {
		csvReader := csvs.generator()
		err := csvReader.Open(fileName, csvs.sep, collumnCount)
		if err != nil {
			// maybe a log to view if failed to open file
			continue // try read the rest
		}
		if err = func() error { // to execute defer corectly
			defer csvReader.Close()
			for record, err := csvReader.Read(); err != io.EOF; record, err = csvReader.Read() {
				if err != nil {
					log.Printf("bad line in %s, %s\n", fileName, err.Error())
					return err
				}
				if item, err := csvLoad(listType, record); err != nil {
					log.Printf("error loading %s: %v", "", err)
					return err
				} else {
					process(item)
				}
			}
			return nil
		}(); err != nil {
			return err
		}
	}
	return nil
}

func (csvs *CSVStorage) GetTPTimings(tpid, id string) ([]*utils.ApierTPTiming, error) {
	var tpTimings TimingMdls
	if err := csvs.proccesData(TimingMdl{}, csvs.timingsFn, func(tp any) {
		tm := tp.(TimingMdl)
		tm.Tpid = tpid
		tpTimings = append(tpTimings, tm)
	}); err != nil {
		return nil, err
	}
	return tpTimings.AsTPTimings(), nil
}

func (csvs *CSVStorage) GetTPDestinations(tpid, id string) ([]*utils.TPDestination, error) {
	var tpDests DestinationMdls
	if err := csvs.proccesData(DestinationMdl{}, csvs.destinationsFn, func(tp any) {
		d := tp.(DestinationMdl)
		d.Tpid = tpid
		tpDests = append(tpDests, d)
	}); err != nil {
		return nil, err
	}
	return tpDests.AsTPDestinations(), nil
}

func (csvs *CSVStorage) GetTPRates(tpid, id string) ([]*utils.TPRateRALs, error) {
	var tpRates RateMdls
	if err := csvs.proccesData(RateMdl{}, csvs.ratesFn, func(tp any) {
		r := tp.(RateMdl)
		r.Tpid = tpid
		tpRates = append(tpRates, r)
	}); err != nil {
		return nil, err
	}
	return tpRates.AsTPRates()
}

func (csvs *CSVStorage) GetTPDestinationRates(tpid, id string, p *utils.Paginator) ([]*utils.TPDestinationRate, error) {
	var tpDestinationRates DestinationRateMdls
	if err := csvs.proccesData(DestinationRateMdl{}, csvs.destinationratesFn, func(tp any) {
		dr := tp.(DestinationRateMdl)
		dr.Tpid = tpid
		tpDestinationRates = append(tpDestinationRates, dr)
	}); err != nil {
		return nil, err
	}
	return tpDestinationRates.AsTPDestinationRates(), nil
}

func (csvs *CSVStorage) GetTPRatingPlans(tpid, id string, p *utils.Paginator) ([]*utils.TPRatingPlan, error) {
	var tpRatingPlans RatingPlanMdls
	if err := csvs.proccesData(RatingPlanMdl{}, csvs.destinationratetimingsFn, func(tp any) {
		rp := tp.(RatingPlanMdl)
		rp.Tpid = tpid
		tpRatingPlans = append(tpRatingPlans, rp)
	}); err != nil {
		return nil, err
	}
	return tpRatingPlans.AsTPRatingPlans(), nil
}

func (csvs *CSVStorage) GetTPRatingProfiles(filter *utils.TPRatingProfile) ([]*utils.TPRatingProfile, error) {
	var tpRatingProfiles RatingProfileMdls
	if err := csvs.proccesData(RatingProfileMdl{}, csvs.ratingprofilesFn, func(tp any) {
		rpf := tp.(RatingProfileMdl)
		if filter != nil {
			rpf.Tpid = filter.TPid
			rpf.Loadid = filter.LoadId
		}
		tpRatingProfiles = append(tpRatingProfiles, rpf)
	}); err != nil {
		return nil, err
	}
	return tpRatingProfiles.AsTPRatingProfiles(), nil
}

func (csvs *CSVStorage) GetTPSharedGroups(tpid, id string) ([]*utils.TPSharedGroups, error) {
	var tpSharedGroups SharedGroupMdls
	if err := csvs.proccesData(SharedGroupMdl{}, csvs.sharedgroupsFn, func(tp any) {
		sg := tp.(SharedGroupMdl)
		sg.Tpid = tpid
		tpSharedGroups = append(tpSharedGroups, sg)
	}); err != nil {
		return nil, err
	}
	return tpSharedGroups.AsTPSharedGroups(), nil
}

func (csvs *CSVStorage) GetTPActions(tpid, id string) ([]*utils.TPActions, error) {
	var tpActions ActionMdls
	if err := csvs.proccesData(ActionMdl{}, csvs.actionsFn, func(tp any) {
		a := tp.(ActionMdl)
		a.Tpid = tpid
		tpActions = append(tpActions, a)
	}); err != nil {
		return nil, err
	}
	return tpActions.AsTPActions(), nil
}

func (csvs *CSVStorage) GetTPActionPlans(tpid, id string) ([]*utils.TPActionPlan, error) {
	var tpActionPlans ActionPlanMdls
	if err := csvs.proccesData(ActionPlanMdl{}, csvs.actiontimingsFn, func(tp any) {
		ap := tp.(ActionPlanMdl)
		ap.Tpid = tpid
		tpActionPlans = append(tpActionPlans, ap)
	}); err != nil {
		return nil, err
	}
	return tpActionPlans.AsTPActionPlans(), nil
}

func (csvs *CSVStorage) GetTPActionTriggers(tpid, id string) ([]*utils.TPActionTriggers, error) {
	var tpActionTriggers ActionTriggerMdls
	if err := csvs.proccesData(ActionTriggerMdl{}, csvs.actiontriggersFn, func(tp any) {
		at := tp.(ActionTriggerMdl)
		at.Tpid = tpid
		tpActionTriggers = append(tpActionTriggers, at)
	}); err != nil {
		return nil, err
	}
	return tpActionTriggers.AsTPActionTriggers(), nil
}

func (csvs *CSVStorage) GetTPAccountActions(filter *utils.TPAccountActions) ([]*utils.TPAccountActions, error) {
	var tpAccountActions AccountActionMdls
	if err := csvs.proccesData(AccountActionMdl{}, csvs.accountactionsFn, func(tp any) {
		aa := tp.(AccountActionMdl)
		if filter != nil {
			aa.Tpid = filter.TPid
			aa.Loadid = filter.LoadId
		}
		tpAccountActions = append(tpAccountActions, aa)
	}); err != nil {
		return nil, err
	}
	return tpAccountActions.AsTPAccountActions(), nil
}

func (csvs *CSVStorage) GetTPResources(tpid, tenant, id string) ([]*utils.TPResourceProfile, error) {
	var tpResLimits ResourceMdls
	if err := csvs.proccesData(ResourceMdl{}, csvs.resProfilesFn, func(tp any) {
		tpLimit := tp.(ResourceMdl)
		tpLimit.Tpid = tpid
		tpResLimits = append(tpResLimits, &tpLimit)
	}); err != nil {
		return nil, err
	}
	return tpResLimits.AsTPResources(), nil
}

func (csvs *CSVStorage) GetTPIPs(tpid, tenant, id string) ([]*utils.TPIPProfile, error) {
	var mdls IPMdls
	if err := csvs.proccesData(IPMdl{}, csvs.ipProfilesFn, func(tp any) {
		mdl := tp.(IPMdl)
		mdl.Tpid = tpid
		mdls = append(mdls, &mdl)
	}); err != nil {
		return nil, err
	}
	return mdls.AsTPIPs(), nil
}

func (csvs *CSVStorage) GetTPStats(tpid, tenant, id string) ([]*utils.TPStatProfile, error) {
	var tpStats StatMdls
	if err := csvs.proccesData(StatMdl{}, csvs.statsFn, func(tp any) {
		tPstats := tp.(StatMdl)
		tPstats.Tpid = tpid
		tpStats = append(tpStats, &tPstats)
	}); err != nil {
		return nil, err
	}
	return tpStats.AsTPStats(), nil
}

func (csvs *CSVStorage) GetTPTrends(tpid, tenant, id string) ([]*utils.TPTrendsProfile, error) {
	var tpTrends TrendsMdls
	if err := csvs.proccesData(TrendsMdl{}, csvs.trendsFn, func(tp any) {
		tPTrends := tp.(TrendsMdl)
		tPTrends.Tpid = tpid
		tpTrends = append(tpTrends, &tPTrends)
	}); err != nil {
		return nil, err
	}
	return tpTrends.AsTPTrends(), nil
}

func (csvs *CSVStorage) GetTPRankings(tpid, tenant, id string) ([]*utils.TPRankingProfile, error) {
	var tpRankings RankingsMdls
	if err := csvs.proccesData(RankingsMdl{}, csvs.rankingsFn, func(tp any) {
		tPrankings := tp.(RankingsMdl)
		tPrankings.Tpid = tpid
		tpRankings = append(tpRankings, &tPrankings)
	}); err != nil {
		return nil, err
	}
	return tpRankings.AsTPRanking(), nil
}

func (csvs *CSVStorage) GetTPThresholds(tpid, tenant, id string) ([]*utils.TPThresholdProfile, error) {
	var tpThreshold ThresholdMdls
	if err := csvs.proccesData(ThresholdMdl{}, csvs.thresholdsFn, func(tp any) {
		tHresholdCfg := tp.(ThresholdMdl)
		tHresholdCfg.Tpid = tpid
		tpThreshold = append(tpThreshold, &tHresholdCfg)
	}); err != nil {
		return nil, err
	}
	return tpThreshold.AsTPThreshold(), nil
}

func (csvs *CSVStorage) GetTPFilters(tpid, tenant, id string) ([]*utils.TPFilterProfile, error) {
	var tpFilter FilterMdls
	if err := csvs.proccesData(FilterMdl{}, csvs.filterFn, func(tp any) {
		fIlterCfg := tp.(FilterMdl)
		fIlterCfg.Tpid = tpid
		tpFilter = append(tpFilter, &fIlterCfg)
	}); err != nil {
		return nil, err
	}
	return tpFilter.AsTPFilter(), nil
}

func (csvs *CSVStorage) GetTPRoutes(tpid, tenant, id string) ([]*utils.TPRouteProfile, error) {
	var tpRoutes RouteMdls
	if err := csvs.proccesData(RouteMdl{}, csvs.routeProfilesFn, func(tp any) {
		suppProfile := tp.(RouteMdl)
		suppProfile.Tpid = tpid
		tpRoutes = append(tpRoutes, &suppProfile)
	}); err != nil {
		return nil, err
	}
	return tpRoutes.AsTPRouteProfile(), nil
}

func (csvs *CSVStorage) GetTPAttributes(tpid, tenant, id string) ([]*utils.TPAttributeProfile, error) {
	var tpAls AttributeMdls
	if err := csvs.proccesData(AttributeMdl{}, csvs.attributeProfilesFn, func(tp any) {
		attributeProfile := tp.(AttributeMdl)
		attributeProfile.Tpid = tpid
		tpAls = append(tpAls, &attributeProfile)
	}); err != nil {
		return nil, err
	}
	return tpAls.AsTPAttributes(), nil
}

func (csvs *CSVStorage) GetTPChargers(tpid, tenant, id string) ([]*utils.TPChargerProfile, error) {
	var tpCPPs ChargerMdls
	if err := csvs.proccesData(ChargerMdl{}, csvs.chargerProfilesFn, func(tp any) {
		cpp := tp.(ChargerMdl)
		cpp.Tpid = tpid
		tpCPPs = append(tpCPPs, &cpp)
	}); err != nil {
		return nil, err
	}
	return tpCPPs.AsTPChargers(), nil
}

func (csvs *CSVStorage) GetTPDispatcherProfiles(tpid, tenant, id string) ([]*utils.TPDispatcherProfile, error) {
	var tpDPPs DispatcherProfileMdls
	if err := csvs.proccesData(DispatcherProfileMdl{}, csvs.dispatcherProfilesFn, func(tp any) {
		dpp := tp.(DispatcherProfileMdl)
		dpp.Tpid = tpid
		tpDPPs = append(tpDPPs, &dpp)
	}); err != nil {
		return nil, err
	}
	return tpDPPs.AsTPDispatcherProfiles(), nil
}

func (csvs *CSVStorage) GetTPDispatcherHosts(tpid, tenant, id string) ([]*utils.TPDispatcherHost, error) {
	var tpDDHs DispatcherHostMdls
	if err := csvs.proccesData(DispatcherHostMdl{}, csvs.dispatcherHostsFn, func(tp any) {
		dpp := tp.(DispatcherHostMdl)
		dpp.Tpid = tpid
		tpDDHs = append(tpDDHs, &dpp)
	}); err != nil {
		return nil, err
	}
	result, err := tpDDHs.AsTPDispatcherHosts()
	if err != nil {
		return nil, err
	}
	return result, nil
}

func (csvs *CSVStorage) GetTpIds(colName string) ([]string, error) {
	return nil, utils.ErrNotImplemented
}

func (csvs *CSVStorage) GetTpTableIds(tpid, table string,
	distinct utils.TPDistinctIds, filters map[string]string, p *utils.PaginatorWithSearch) ([]string, error) {
	return nil, utils.ErrNotImplemented
}

type csvReaderCloser interface {
	Open(data string, sep rune, nrFields int) (err error)
	Read() (record []string, err error)
	Close()
}

func NewCsvFile() csvReaderCloser {
	return &csvFile{}
}

type csvFile struct {
	csvReader *csv.Reader
	fp        *os.File
}

func (c *csvFile) Open(fn string, sep rune, nrFields int) (err error) {
	c.fp, err = os.Open(fn)
	if err != nil {
		return
	}
	c.csvReader = csv.NewReader(c.fp)
	c.csvReader.Comma = sep
	c.csvReader.Comment = utils.CommentChar
	c.csvReader.FieldsPerRecord = nrFields
	return
}

func (c *csvFile) Read() (record []string, err error) {
	return c.csvReader.Read()
}

func (c *csvFile) Close() {
	if c.fp != nil {
		c.fp.Close()
	}
}

func NewCsvString() csvReaderCloser {
	return &csvString{}
}

type csvString struct {
	csvReader *csv.Reader
}

func (c *csvString) Open(data string, sep rune, nrFields int) (err error) {
	c.csvReader = csv.NewReader(strings.NewReader(data))
	c.csvReader.Comma = sep
	c.csvReader.Comment = utils.CommentChar
	c.csvReader.FieldsPerRecord = nrFields
	return
}

func (c *csvString) Read() (record []string, err error) {
	return c.csvReader.Read()
}

func (c *csvString) Close() { // no need for close
}

// Google

// Retrieve a token, saves the token, then returns the generated client.
func getClient(cfg *oauth2.Config, configPath string) (*http.Client, error) {
	// The file token.json stores the user's access and refresh tokens, and is
	// created automatically when the authorization flow completes for the first
	// time.
	tok := &oauth2.Token{}
	raw, err := getCfgJSONData(config.CgrConfig().LoaderCgrCfg().GapiToken)
	if err != nil {
		tok, err = getTokenFromWeb(cfg)
		if err != nil {
			return nil, err
		}
		path2TokFileb := config.CgrConfig().LoaderCgrCfg().GapiToken
		path2TokFile := string(path2TokFileb[1 : len(path2TokFileb)-1])
		if err := os.MkdirAll(filepath.Dir(path2TokFile), os.FileMode(0777)); err != nil { // create the directory if not exists
			return nil, err
		}
		saveToken(path2TokFile, tok)
	} else if err = json.Unmarshal(raw, tok); err != nil {
		return nil, err
	}
	return cfg.Client(context.Background(), tok), nil
}

// Request a token from the web, then returns the retrieved token.
func getTokenFromWeb(config *oauth2.Config) (*oauth2.Token, error) {
	authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline)
	fmt.Printf("Go to the following link in your browser then type the "+
		"authorization code: \n%v\n", authURL)

	var authCode string
	if _, err := fmt.Scan(&authCode); err != nil {
		err = fmt.Errorf("Unable to read authorization code: %v", err)
		return nil, err
	}

	tok, err := config.Exchange(context.TODO(), authCode)
	if err != nil {
		err = fmt.Errorf("Unable to retrieve token from web: %v", err)
	}
	return tok, err
}

// Saves a token to a file path.
func saveToken(path string, token *oauth2.Token) {
	fmt.Printf("Saving credential file to: %s\n", path)
	f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
	if err != nil {
		log.Printf("Unable to cache oauth token: %v\n", err)
		return
	}
	defer f.Close()
	json.NewEncoder(f).Encode(token)
}

func getCfgJSONData(raw json.RawMessage) (data []byte, err error) {
	if len(raw) == 0 {
		return
	}
	if raw[0] == '{' && raw[len(raw)-1] == '}' {
		data = raw
		return
	}
	dataPath := string(raw[1 : len(raw)-1])
	if !strings.HasSuffix(dataPath, utils.JSNSuffix) {
		dataPath = path.Join(dataPath, utils.GoogleCredentialsFileName)
	}
	return os.ReadFile(dataPath)
}

func newSheet() (sht *sheets.Service, err error) { //*google_api
	var cred []byte
	var cfgPathDir string
	if cred, err = getCfgJSONData(config.CgrConfig().LoaderCgrCfg().GapiCredentials); err != nil {
		err = fmt.Errorf("Unable to read client secret file: %v", err)
		return
	}
	// If modifying these scopes, delete your previously saved token.json.
	config, err := google.ConfigFromJSON(cred, "https://www.googleapis.com/auth/spreadsheets.readonly")
	if err != nil {
		err = fmt.Errorf("Unable to parse client secret file to config: %v", err)
		return
	}
	client, err := getClient(config, cfgPathDir)
	if err != nil {
		return nil, err
	}
	sht, err = sheets.NewService(context.Background(), option.WithHTTPClient(client))
	if err != nil {
		err = fmt.Errorf("Unable to retrieve Sheets client: %v", err)
	}
	return
}

func getSpreatsheetTabs(spreadsheetID string, srv *sheets.Service) (sheetsName utils.StringSet, err error) {
	sheetsName = make(utils.StringSet)
	sht, err := srv.Spreadsheets.Get(spreadsheetID).Do()
	if err != nil {
		err = fmt.Errorf("Unable get the information about spreadsheet because: %v", err)
		return
	}
	for _, sheet := range sht.Sheets {
		sheetsName.Add(sheet.Properties.Title)
	}
	return
}

type csvGoogle struct {
	spreadsheetID string
	srv           *sheets.Service
	response      *sheets.ValueRange
	indx          int
	nrFields      int
}

func (c *csvGoogle) Open(data string, sep rune, nrFields int) (err error) {
	c.response, err = c.srv.Spreadsheets.Values.Get(c.spreadsheetID, data).Do()
	if err != nil {
		return
	}

	if len(c.response.Values) == 0 {
		return utils.ErrNotFound
	}
	c.nrFields = nrFields
	return
}

func (c *csvGoogle) getNextRow() (row []any, err error) {
	if len(c.response.Values) <= c.indx {
		return nil, io.EOF
	}
	row = c.response.Values[c.indx]
	c.indx++
	if len(row) == 0 {
		return c.getNextRow()
	}
	return
}

func (c *csvGoogle) Read() (record []string, err error) {
	row, err := c.getNextRow()
	if err != nil {
		return
	}
	record = make([]string, c.nrFields)
	for i := 0; i < c.nrFields; i++ {
		if i < len(row) {
			record[i] = utils.IfaceAsString(row[i])
			if i == 0 && strings.HasPrefix(record[i], "#") {
				return c.Read() // ignore row if starts with #
			}
		} else {
			record[i] = ""
		}
	}
	return
}

func (c *csvGoogle) Close() { // no need for close
}

type csvURL struct {
	csvReader *csv.Reader
	page      io.ReadCloser
}

func (c *csvURL) Open(fn string, sep rune, nrFields int) (err error) {
	if _, err = url.ParseRequestURI(fn); err != nil {
		return
	}
	var myClient = &http.Client{
		Timeout: config.CgrConfig().GeneralCfg().ReplyTimeout,
	}
	var req *http.Response
	req, err = myClient.Get(fn)
	if err != nil {
		return utils.ErrPathNotReachable(fn)
	}
	if req.StatusCode != http.StatusOK {
		return utils.ErrNotFound
	}
	c.page = req.Body

	c.csvReader = csv.NewReader(c.page)
	c.csvReader.Comma = sep
	c.csvReader.Comment = utils.CommentChar
	c.csvReader.FieldsPerRecord = nrFields
	return
}

func (c *csvURL) Read() (record []string, err error) {
	return c.csvReader.Read()
}

func (c *csvURL) Close() {
	if c.page != nil {
		c.page.Close()
	}
}
