package otpauth

import (
	"bytes"
	"encoding/base64"
	"encoding/binary"
	"errors"
	"fmt"
	"io"
	"net/url"
	"strings"

	"github.com/creachadair/wirepb"
)

// ParseMigrationURL parses an otpauth-migration URL in the format generated by
// the Google Authenticator for "exported" configurations. Typically these URLs
// are embedded as QR codes, encoding a proprietary URL in this format:
//
//	otpauth-migration://offline?data=<content>
//
// The content is a protocol buffer message encoded as base64 in standard
// encoding.  Note that a single migration URL may encode multiple OTP
// settings; on success this function returns all the otpauth URLs encoded by
// the content.  It will always return at least one URL, or report an error.
func ParseMigrationURL(s string) ([]*URL, error) {
	rest, ok := strings.CutPrefix(s, "otpauth-migration://")
	if !ok {
		return nil, errors.New("missing otpauth-migration schema prefix")
	}
	content, ok := strings.CutPrefix(rest, "offline?data=")
	if !ok {
		return nil, errors.New("unrecognized path format")
	}
	dec, err := url.QueryUnescape(content)
	if err != nil {
		return nil, fmt.Errorf("invalid data: %w", err)
	}
	bits, err := base64.StdEncoding.DecodeString(dec)
	if err != nil {
		return nil, fmt.Errorf("invalid base64: %w", err)
	}
	return parseMigrations(bits)
}

/*
The content of a migration URL is a wire-format protocol buffer message.

I don't want to take a dependency on protobuf, since that pulls in a lot of
other expensive Google nonsense, but fortunately the message structure is
fairly simple:

   message Content {
      repeated Params params = 1;

      // ... other fields not of interest.
      // If you're exporting more data than can fit in one QR code, the app may
      // split up the export into multiple codes. There are some fields here to
      // keep track of that, but they aren't relevant here.
   }

   message Params {
      bytes  secret    = 1;
      string account   = 2;
      string issuer    = 3;
      int32  algorithm = 4;  // 0: unspec, 1: SHA1, 2: SHA256, 3: SHA512, 4: MD5
      int32  digits    = 5;  // 0: unspec, 1: 6 digits, 2: 8 digits (typical Google)
      int32  type      = 6;  // 0: unspec, 1: HOTP, 2: TOTP
      uint64 counter   = 7;
   }

So here we just unpack the wire format directly.
*/

// parseMigrations parses data as a wire-format protobuf message in the Content
// format described above, and returns a single URL for each instance of the
// Params found therein. Other fields of the message are ignored.
func parseMigrations(data []byte) ([]*URL, error) {
	const paramsField = 1

	var out []*URL
	s := wirepb.NewScanner(bytes.NewReader(data))
	for s.Next() == nil {
		if s.ID() == paramsField {
			u, err := parseParams(s.Data())
			if err != nil {
				return nil, err
			}
			out = append(out, u)
		}
	}
	if s.Err() != io.EOF {
		return nil, s.Err()
	} else if len(out) == 0 {
		return nil, errors.New("no URLs found")
	}
	return out, nil
}

func parseParams(data []byte) (*URL, error) {
	const (
		secretField = 1 + iota
		accountField
		issuerField
		algorithmField
		digitsField
		typeField
		counterField
	)

	var out = URL{Algorithm: defaultAlgorithm, Digits: defaultDigits, Period: defaultPeriod}
	s := wirepb.NewScanner(bytes.NewReader(data))
	for s.Next() == nil {
		switch s.ID() {
		case secretField:
			out.SetSecret(s.Data())
		case accountField:
			out.Account = string(s.Data())
		case issuerField:
			out.Issuer = string(s.Data())
		case algorithmField:
			switch v, _ := binary.Uvarint(s.Data()); v {
			case 1:
				out.Algorithm = "SHA1"
			case 2:
				out.Algorithm = "SHA256"
			case 3:
				out.Algorithm = "SHA512"
			case 4:
				out.Algorithm = "MD5"
			default:
				return nil, fmt.Errorf("unknown algorithm code %d", v)
			}
		case digitsField:
			switch v, _ := binary.Uvarint(s.Data()); v {
			case 1:
				out.Digits = 6
			case 2:
				out.Digits = 8
			default:
				return nil, fmt.Errorf("unknown digits code %d", v)
			}
		case typeField:
			switch v, _ := binary.Uvarint(s.Data()); v {
			case 1:
				out.Type = "hotp"
			case 2:
				out.Type = "totp"
			default:
				return nil, fmt.Errorf("unknown type code %d", v)
			}
		case counterField:
			v, n := binary.Uvarint(s.Data())
			if n <= 0 {
				return nil, errors.New("invalid counter value")
			}
			out.Counter = v
		}
	}
	if s.Err() != io.EOF {
		return nil, s.Err()
	}
	return &out, nil
}
