package main

import (
	"database/sql"
	"sync"
	"time"

	"github.com/ipfs/go-cid"
	_ "github.com/mattn/go-sqlite3"

	"github.com/filecoin-project/lotus/chain/address"
	"github.com/filecoin-project/lotus/chain/types"
)

type storage struct {
	db *sql.DB

	headerLk sync.Mutex
}

func openStorage() (*storage, error) {
	db, err := sql.Open("sqlite3", "./chainwatch.db")
	if err != nil {
		return nil, err
	}

	st := &storage{db: db}

	return st, st.setup()
}

func (st *storage) setup() error {
	tx, err := st.db.Begin()
	if err != nil {
		return err
	}
	_, err = tx.Exec(`
create table if not exists actors
  (
	id text not null,
	code text not null,
	head text not null,
	nonce int not null,
	balance text not null,
	stateroot text
		constraint actors_blocks_stateroot_fk
			references blocks (parentStateRoot),
	constraint actors_pk
		primary key (id, nonce, balance, stateroot)
  );
  
create index if not exists actors_id_index
	on actors (id);

create table if not exists id_address_map
(
	id text not null
		constraint id_address_map_actors_id_fk
			references actors (id),
	address text not null,
	constraint id_address_map_pk
		primary key (id, address)
);

create index if not exists id_address_map_address_index
	on id_address_map (address);

create index if not exists id_address_map_id_index
	on id_address_map (id);

create table if not exists messages
(
	cid text not null
		constraint messages_pk
			primary key,
	"from" text not null
		constraint messages_id_address_map_from_fk
			references id_address_map (address),
	"to" text not null
		constraint messages_id_address_map_to_fk
			references id_address_map (address),
	nonce int not null,
	value text not null,
	gasprice int not null,
	gaslimit int not null,
	method int,
	params blob
);

create unique index if not exists messages_cid_uindex
	on messages (cid);

create table if not exists blocks
(
	cid text not null
		constraint blocks_pk
			primary key,
	parentWeight numeric not null,
	parentStateRoot text not null,
	height int not null,
	miner text not null
		constraint blocks_id_address_map_miner_fk
			references id_address_map (address),
	timestamp int not null
);

create unique index if not exists block_cid_uindex
	on blocks (cid);

create table if not exists blocks_synced
(
	cid text not null
		constraint blocks_synced_pk
			primary key
		constraint blocks_synced_blocks_cid_fk
			references blocks
);

create unique index if not exists blocks_synced_cid_uindex
	on blocks_synced (cid);

create table if not exists block_parents
(
	block text not null
		constraint block_parents_blocks_cid_fk
			references blocks,
	parent text not null
		constraint block_parents_blocks_cid_fk_2
			references blocks
);

create unique index if not exists block_parents_block_parent_uindex
	on block_parents (block, parent);

create unique index if not exists blocks_cid_uindex
	on blocks (cid);
	
create table if not exists block_messages
(
	block text not null
		constraint block_messages_blk_fk
			references blocks (cid),
	message text not null
		constraint block_messages_msg_fk
			references messages,
	constraint block_messages_pk
		primary key (block, message)
);

create table if not exists mpool_messages
(
	msg text not null
		constraint mpool_messages_pk
			primary key
		constraint mpool_messages_messages_cid_fk
			references messages,
	add_ts int not null
);

create unique index if not exists mpool_messages_msg_uindex
	on mpool_messages (msg);

create table if not exists miner_heads
(
	head text not null
		constraint miner_heads_actors_head_fk
			references actors (head),
	addr text not null
		constraint miner_heads_actors_id_fk
			references actors (id),
	stateroot text not null
		constraint miner_heads_blocks_stateroot_fk
			references blocks (parentStateRoot),
	sectorset text not null,
	provingset text not null,
	owner text not null,
	worker text not null,
	peerid text not null,
	sectorsize int not null,
	power text not null,
	active int,
	ppe int not null,
	slashed_at int not null,
	constraint miner_heads_id_address_map_owner_fk
		foreign key (owner) references id_address_map (address),
	constraint miner_heads_id_address_map_worker_fk
		foreign key (worker) references id_address_map (address),
	constraint miner_heads_pk
		primary key (head, addr)
);

`)
	if err != nil {
		return err
	}
	return tx.Commit()
}

func (st *storage) hasBlock(bh cid.Cid) bool {
	var exitsts bool
	err := st.db.QueryRow(`select exists (select 1 FROM blocks_synced where cid=?)`, bh.String()).Scan(&exitsts)
	if err != nil {
		log.Error(err)
		return false
	}
	return exitsts
}

func (st *storage) storeActors(actors map[address.Address]map[types.Actor]cid.Cid) error {
	tx, err := st.db.Begin()
	if err != nil {
		return err
	}

	stmt, err := tx.Prepare(`insert into actors (id, code, head, nonce, balance, stateroot) values (?, ?, ?, ?, ?, ?) on conflict do nothing`)
	if err != nil {
		return err
	}
	defer stmt.Close()
	for addr, acts := range actors {
		for act, st := range acts {
			if _, err := stmt.Exec(addr.String(), act.Code.String(), act.Head.String(), act.Nonce, act.Balance.String(), st.String()); err != nil {
				return err
			}
		}
	}

	return tx.Commit()
}

func (st *storage) storeMiners(miners map[minerKey]*minerInfo) error {
	tx, err := st.db.Begin()
	if err != nil {
		return err
	}

	stmt, err := tx.Prepare(`insert into miner_heads (head, addr, stateroot, sectorset, provingset, owner, worker, peerid, sectorsize, power, active, ppe, slashed_at) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) on conflict do nothing`)
	if err != nil {
		return err
	}
	defer stmt.Close()
	for k, i := range miners {
		if _, err := stmt.Exec(
			k.act.Head.String(),
			k.addr.String(),
			k.stateroot.String(),
			i.state.Sectors.String(),
			i.state.ProvingSet.String(),
			i.info.Owner.String(),
			i.info.Worker.String(),
			i.info.PeerID.String(),
			i.info.SectorSize,
			i.state.Power.String(),
			i.state.Active,
			i.state.ElectionPeriodStart,
			i.state.SlashedAt,
		); err != nil {
			return err
		}
	}

	return tx.Commit()
}

func (st *storage) storeHeaders(bhs map[cid.Cid]*types.BlockHeader, sync bool) error {
	st.headerLk.Lock()
	defer st.headerLk.Unlock()

	tx, err := st.db.Begin()
	if err != nil {
		return err
	}

	stmt, err := tx.Prepare(`insert into blocks (cid, parentWeight, parentStateRoot, height, miner, "timestamp") values (?, ?, ?, ?, ?, ?) on conflict do nothing`)
	if err != nil {
		return err
	}
	defer stmt.Close()
	for _, bh := range bhs {
		if _, err := stmt.Exec(bh.Cid().String(), bh.ParentWeight.String(), bh.ParentStateRoot.String(), bh.Height, bh.Miner.String(), bh.Timestamp); err != nil {
			return err
		}
	}

	stmt2, err := tx.Prepare(`insert into block_parents (block, parent) values (?, ?) on conflict do nothing`)
	if err != nil {
		return err
	}
	defer stmt2.Close()
	for _, bh := range bhs {
		for _, parent := range bh.Parents {
			if _, err := stmt2.Exec(bh.Cid().String(), parent.String()); err != nil {
				return err
			}
		}
	}

	if sync {
		stmt, err := tx.Prepare(`insert into blocks_synced (cid) values (?) on conflict do nothing`)
		if err != nil {
			return err
		}
		defer stmt.Close()
		for _, bh := range bhs {
			if _, err := stmt.Exec(bh.Cid().String()); err != nil {
				return err
			}
		}
	}

	return tx.Commit()
}

func (st *storage) storeMessages(msgs map[cid.Cid]*types.Message) error {
	tx, err := st.db.Begin()
	if err != nil {
		return err
	}

	stmt, err := tx.Prepare(`insert into messages (cid, "from", "to", nonce, "value", gasprice, gaslimit, method, params) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) on conflict do nothing`)
	if err != nil {
		return err
	}
	defer stmt.Close()

	for c, m := range msgs {
		if _, err := stmt.Exec(
			c.String(),
			m.From.String(),
			m.To.String(),
			m.Nonce,
			m.Value.String(),
			m.GasPrice.String(),
			m.GasLimit.String(),
			m.Method,
			m.Params,
		); err != nil {
			return err
		}
	}

	return tx.Commit()
}

func (st *storage) storeAddressMap(addrs map[address.Address]address.Address) error {
	tx, err := st.db.Begin()
	if err != nil {
		return err
	}

	stmt, err := tx.Prepare(`insert into id_address_map (id, address) VALUES (?, ?) on conflict do nothing`)
	if err != nil {
		return err
	}
	defer stmt.Close()

	for a, i := range addrs {
		if i == address.Undef {
			continue
		}
		if _, err := stmt.Exec(
			i.String(),
			a.String(),
		); err != nil {
			return err
		}
	}

	return tx.Commit()
}

func (st *storage) storeMsgInclusions(incls map[cid.Cid][]cid.Cid) error {
	tx, err := st.db.Begin()
	if err != nil {
		return err
	}

	stmt, err := tx.Prepare(`insert into block_messages (block, message) VALUES (?, ?) on conflict do nothing`)
	if err != nil {
		return err
	}
	defer stmt.Close()

	for b, msgs := range incls {
		for _, msg := range msgs {
			if _, err := stmt.Exec(
				b.String(),
				msg.String(),
			); err != nil {
				return err
			}
		}
	}

	return tx.Commit()
}

func (st *storage) storeMpoolInclusion(msg cid.Cid) error {
	tx, err := st.db.Begin()
	if err != nil {
		return err
	}

	stmt, err := tx.Prepare(`insert into mpool_messages (msg, add_ts) VALUES (?, ?) on conflict do nothing`)
	if err != nil {
		return err
	}
	defer stmt.Close()

	if _, err := stmt.Exec(
		msg.String(),
		time.Now().Unix(),
	); err != nil {
		return err
	}
	return tx.Commit()
}

func (st *storage) close() error {
	return st.db.Close()
}