package platform

import (
	"context"
	"crypto/rand"
	"errors"
	"fmt"
	"math/big"
	"strconv"
	"strings"
	"time"

	"github.com/Gleipnir-Technology/jet/postgres"
	"github.com/rs/zerolog/log"
	"source.gleipnir.technology/Gleipnir/nidus-sync/db"
	"source.gleipnir.technology/Gleipnir/nidus-sync/db/gen/nidus-sync/public/model"
	tablepublic "source.gleipnir.technology/Gleipnir/nidus-sync/db/gen/nidus-sync/public/table"
	modelpublicreport "source.gleipnir.technology/Gleipnir/nidus-sync/db/gen/nidus-sync/publicreport/model"
	tablepublicreport "source.gleipnir.technology/Gleipnir/nidus-sync/db/gen/nidus-sync/publicreport/table"
	querycomms "source.gleipnir.technology/Gleipnir/nidus-sync/db/query/comms"
	querypublic "source.gleipnir.technology/Gleipnir/nidus-sync/db/query/public"
	querypublicreport "source.gleipnir.technology/Gleipnir/nidus-sync/db/query/publicreport"
	"source.gleipnir.technology/Gleipnir/nidus-sync/lint"
	"source.gleipnir.technology/Gleipnir/nidus-sync/platform/email"
	"source.gleipnir.technology/Gleipnir/nidus-sync/platform/event"
	"source.gleipnir.technology/Gleipnir/nidus-sync/platform/geocode"
	"source.gleipnir.technology/Gleipnir/nidus-sync/platform/publicreport"
	"source.gleipnir.technology/Gleipnir/nidus-sync/platform/text"
	"source.gleipnir.technology/Gleipnir/nidus-sync/platform/types"
)

// GenerateReportID creates a 12-character random string using only unambiguous
// capital letters and numbers
func GenerateReportID() (string, error) {
	// Define character set (no O/0, I/l/1, 2/Z to avoid confusion)
	const charset = "ABCDEFGHJKLMNPQRSTUVWXY3456789"
	const length = 12

	var builder strings.Builder
	builder.Grow(length)

	// Use crypto/rand for secure randomness
	for i := 0; i < length; i++ {
		// Generate a random index within our charset
		n, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
		if err != nil {
			return "", fmt.Errorf("failed to generate random number: %w", err)
		}

		// Add the randomly selected character to our ID
		builder.WriteByte(charset[n.Int64()])
	}

	return builder.String(), nil
}

func PublicReportByIDCompliance(ctx context.Context, report_id string, is_public bool) (*types.PublicReportCompliance, error) {
	result, err := publicreport.ByIDCompliance(ctx, report_id, is_public)
	if err != nil {
		return nil, fmt.Errorf("byidcompliance: %w", err)
	}
	if result == nil {
		return nil, nil
	}
	// Check for evidence if this is a mailer-based compliance request
	crr, err := ComplianceReportRequestFromPublicID(ctx, result.PublicID)
	if err != nil {
		return nil, fmt.Errorf("compliance report request by public id: %w", err)
	}
	if crr != nil {
		result.Concerns = []*types.ConcernComplianceReportRequest{
			&types.ConcernComplianceReportRequest{
				ComplianceReportRequestPublicID: crr.PublicID,
			},
		}
	}
	return result, nil
}
func PublicReportByIDNuisance(ctx context.Context, report_id string, is_public bool) (*types.PublicReportNuisance, error) {
	return publicreport.ByIDNuisance(ctx, report_id, is_public)
}
func PublicReportByIDWater(ctx context.Context, report_id string, is_public bool) (*types.PublicReportWater, error) {
	result, err := publicreport.ByIDWater(ctx, report_id, is_public)
	return &result, err
}
func PublicReportInvalid(ctx context.Context, user User, public_id string) error {
	txn := db.PGInstance.PGXPool
	report, err := querypublicreport.ReportFromPublicID(ctx, txn, public_id)
	if err != nil {
		return fmt.Errorf("query report existence: %w", err)
	}
	if report.OrganizationID != user.Organization.ID {
		return fmt.Errorf("user is from a different organization")
	}

	now := time.Now()
	report_updater := querypublicreport.NewReportUpdater()
	report_updater.Model.Reviewed = &now
	report_updater.Set(tablepublicreport.Report.Reviewed)
	reporter_id := int32(user.ID)
	report_updater.Model.ReviewerID = &reporter_id
	report_updater.Set(tablepublicreport.Report.ReviewerID)
	report_updater.Model.Status = modelpublicreport.Reportstatustype_Invalidated
	report_updater.Set(tablepublicreport.Report.Status)
	err = report_updater.Execute(ctx, txn, report.ID)

	log.Info().Int32("id", report.ID).Msg("Report marked as invalid")
	event.Updated(event.TypeRMOPublicReport, user.Organization.ID, public_id)
	comm, err := querypublic.CommunicationFromReportID(ctx, txn, int64(report.ID))
	if err != nil {
		return fmt.Errorf("communication from report ID %d: %w", report.ID, err)
	}
	event.Updated(event.TypeCommunication, user.Organization.ID, strconv.Itoa(int(comm.ID)))
	return nil
}

func PublicReportMessageCreate(ctx context.Context, user User, public_id, message string) (message_id *int32, err error) {
	txn, err := db.BeginTxn(ctx)
	if err != nil {
		return nil, fmt.Errorf("create txn: %w", err)
	}
	defer lint.LogOnErrRollback(txn.Rollback, ctx, "rollback")

	report, err := querypublicreport.ReportFromPublicID(ctx, db.PGInstance.PGXPool, public_id)
	if err != nil {
		return nil, fmt.Errorf("query report existence: %w", err)
	}
	if report.OrganizationID != user.Organization.ID {
		return nil, fmt.Errorf("user is from a different organization")
	}
	if report.ReporterPhone != "" {
		log.Debug().Str("public_id", public_id).Msg("contacting via phone")
		p, err := text.ParsePhoneNumber(report.ReporterPhone)
		if err != nil {
			return nil, fmt.Errorf("parse phone: %w", err)
		}
		msg_id, err := text.ReportMessage(ctx, txn, int32(user.ID), int32(report.ID), *p, message)
		if err != nil {
			return nil, fmt.Errorf("send text: %w", err)
		}
		if err := txn.Commit(ctx); err != nil {
			return nil, fmt.Errorf("commit: %w", err)
		}
		//log.Debug().Int32("msg_id", *msg_id).Msg("Created text.ReportMessage")
		return msg_id, nil
	} else if report.ReporterEmail != "" {
		msg_id, err := email.ReportMessage(ctx, int32(user.ID), public_id, report.ReporterEmail, message)
		if err != nil {
			return nil, fmt.Errorf("send email: %w", err)
		}
		if err := txn.Commit(ctx); err != nil {
			return nil, fmt.Errorf("commit: %w", err)
		}
		return msg_id, nil
	} else {
		log.Debug().Str("public_id", public_id).Msg("contacting via email")
		return nil, errors.New("no contact methods available")
	}
}
func PublicReportUpdateCompliance(ctx context.Context, public_id string, report_updates querypublicreport.ReportUpdater, compliance_updates querypublicreport.ComplianceUpdater, address *types.Address, location *types.Location, reporter *types.Contact) error {
	txn, err := db.BeginTxn(ctx)
	if err != nil {
		return fmt.Errorf("create txn: %w", err)
	}
	defer lint.LogOnErrRollback(txn.Rollback, ctx, "rollback")
	report, err := querypublicreport.ReportFromPublicID(ctx, txn, public_id)
	if err != nil {
		return fmt.Errorf("query report existence: %w", err)
	}
	compliance, err := querypublicreport.ComplianceFromID(ctx, txn, int64(report.ID))
	if err != nil {
		return fmt.Errorf("find compliance %d: %w", report.ID, err)
	}
	// Don't allow modifying of the submission date if it's set
	var comm_id int
	if compliance_updates.Has(tablepublicreport.Compliance.Submitted) {
		if compliance.Submitted != nil {
			compliance_updates.Unset(tablepublicreport.Compliance.Submitted)
		} else {
			comm := model.Communication{
				Created:            time.Now(),
				OrganizationID:     report.OrganizationID,
				ResponseEmailLogID: nil,
				ResponseTextLogID:  nil,
				SourceEmailLogID:   nil,
				SourceReportID:     &report.ID,
				SourceTextLogID:    nil,
				Status:             model.Communicationstatus_New,
			}
			comm, err = querypublic.CommunicationInsert(ctx, txn, comm)
			if err != nil {
				return fmt.Errorf("insert communication: %w", err)
			}
			comm_log := model.CommunicationLogEntry{
				CommunicationID: comm.ID,
				Created:         time.Now(),
				Type:            model.Communicationlogentry_Created,
				User:            nil,
			}
			comm_log, err = querypublic.CommunicationLogEntryInsert(ctx, txn, comm_log)
			if err != nil {
				return fmt.Errorf("insert communication log entry: %w", err)
			}
			log.Debug().Int32("id", comm.ID).Msg("inserted new communication")
			comm_id = int(comm.ID)
		}
	}

	// Avoid attempting to perform an empty update
	if address != nil {
		report_updates.Model.AddressGid = address.GID
		report_updates.Set(tablepublicreport.Report.AddressGid)
		report_updates.Model.AddressRaw = address.Raw
		report_updates.Set(tablepublicreport.Report.AddressRaw)
	}
	err = report_updates.Execute(ctx, txn, int64(report.ID))
	if err != nil {
		return fmt.Errorf("update report: %w", err)
	}
	err = compliance_updates.Execute(ctx, txn, int64(compliance.ReportID))
	if err != nil {
		return fmt.Errorf("update compliance: %w", err)
	}
	if address != nil {
		err = publicReportUpdateAddressID(ctx, txn, report, *address)
		if err != nil {
			return fmt.Errorf("update address: %w", err)
		}
	}
	if location != nil {
		err = publicReportUpdateLocation(ctx, txn, report.ID, *location)
		if err != nil {
			return fmt.Errorf("update location: %w", err)
		}
	}
	if reporter != nil {
		if reporter.Name != "" {
			err = querycomms.ContactUpdateName(ctx, txn, int64(report.ReporterContactID), reporter.Name)
			if err != nil {
				return fmt.Errorf("update contact: %w", err)
			}
		} else {
			log.Warn().Int32("report.id", report.ID).Msg("not sure what to do with empty reporter name")
		}
	}
	if err := txn.Commit(ctx); err != nil {
		return fmt.Errorf("commit: %w", err)
	}
	if comm_id != 0 {
		event.Created(
			event.TypeCommunication,
			report.OrganizationID,
			strconv.Itoa(comm_id),
		)
	}
	return nil
}
func PublicReportsForOrganization(ctx context.Context, org_id int32, is_public bool) ([]types.PublicReport, error) {
	return publicreport.UnreviewedForOrganization(ctx, db.PGInstance.PGXPool, int64(org_id), is_public)
}
func PublicReportsFromIDs(ctx context.Context, report_ids []int64) ([]modelpublicreport.Report, error) {
	return querypublicreport.ReportsFromIDs(ctx, report_ids)
}
func PublicReportComplianceCreate(ctx context.Context, setter_report modelpublicreport.Report, setter_compliance modelpublicreport.Compliance, org_id int32) (modelpublicreport.Report, error) {
	return publicReportCreate(ctx, setter_report, nil, nil, nil, org_id, func(ctx context.Context, txn db.Ex, report_id int32) error {
		setter_compliance.ReportID = report_id
		_, err := querypublicreport.ComplianceInsert(ctx, txn, setter_compliance)
		if err != nil {
			return fmt.Errorf("Failed to create compliance database record: %w", err)
		}
		return nil
	})
}
func PublicReportImageCreate(ctx context.Context, public_id string, images []ImageUpload) error {
	txn, err := db.BeginTxn(ctx)
	if err != nil {
		return fmt.Errorf("create txn: %w", err)
	}
	defer lint.LogOnErrRollback(txn.Rollback, ctx, "rollback")

	report, err := querypublicreport.ReportFromPublicID(ctx, db.PGInstance.PGXPool, public_id)
	if err != nil {
		return fmt.Errorf("report from ID: %w", err)
	}
	saved_images, err := saveImageUploads(ctx, txn, images)
	if err != nil {
		return fmt.Errorf("Failed to save image uploads: %w", err)
	}
	if len(saved_images) > 0 {
		report_images := make([]modelpublicreport.ReportImage, len(saved_images))
		for i, image := range saved_images {
			report_images[i] = modelpublicreport.ReportImage{
				ImageID:  image.ID,
				ReportID: report.ID,
			}
		}
		_, err := querypublicreport.ReportImagesInsert(ctx, txn, report_images)
		if err != nil {
			return fmt.Errorf("Failed to save reference to images: %w", err)
		}
		log.Info().Int("len", len(images)).Msg("saved uploaded images")
	}
	if err := txn.Commit(ctx); err != nil {
		return fmt.Errorf("commit: %w", err)
	}
	return nil
}
func PublicReportNuisanceCreate(ctx context.Context, setter_report modelpublicreport.Report, setter_nuisance modelpublicreport.Nuisance, location types.Location, address Address, images []ImageUpload) (modelpublicreport.Report, error) {
	return publicReportCreate(ctx, setter_report, &location, &address, images, 0, func(ctx context.Context, txn db.Ex, report_id int32) error {
		setter_nuisance.ReportID = report_id
		_, err := querypublicreport.NuisanceInsert(ctx, txn, setter_nuisance)
		if err != nil {
			return fmt.Errorf("Failed to create nuisance database record: %w", err)
		}
		return nil
	})
}

func PublicReportWaterCreate(ctx context.Context, setter_report modelpublicreport.Report, setter_water modelpublicreport.Water, location types.Location, address Address, images []ImageUpload) (modelpublicreport.Report, error) {
	return publicReportCreate(ctx, setter_report, &location, &address, images, 0, func(ctx context.Context, txn db.Ex, report_id int32) error {
		setter_water.ReportID = report_id
		_, err := querypublicreport.WaterInsert(ctx, txn, setter_water)
		if err != nil {
			return fmt.Errorf("Failed to create water database record: %w", err)
		}
		return nil
	})
}
func PublicReportTypeByID(ctx context.Context, public_id string) (string, error) {
	report, err := querypublicreport.ReportFromPublicID(ctx, db.PGInstance.PGXPool, public_id)
	if err != nil {
		return "", fmt.Errorf("query report '%s': %w", public_id, err)
	}
	return report.ReportType.String(), nil
}

type funcSetReportDetail = func(context.Context, db.Ex, int32) error

func publicReportCreate(ctx context.Context, setter_report modelpublicreport.Report, location *types.Location, address *Address, images []ImageUpload, organization_id int32, detail_setter funcSetReportDetail) (result modelpublicreport.Report, err error) {
	txn, err := db.BeginTxn(ctx)
	if err != nil {
		return result, fmt.Errorf("create txn: %w", err)
	}
	defer lint.LogOnErrRollback(txn.Rollback, ctx, "rollback")

	if setter_report.PublicID == "" {
		public_id, err := GenerateReportID()
		if err != nil {
			return result, fmt.Errorf("create public ID: %w", err)
		}
		setter_report.PublicID = public_id
	}

	var addr *types.Address
	if address != nil {
		if address.GID != "" {
			addr_existing, err := geocode.EnsureAddress(ctx, txn, *address)
			if err != nil {
				return result, fmt.Errorf("Failed to ensure address: %w", err)
			}
			addr = &addr_existing
		} else if address.Raw != "" {
			geo_res, err := geocode.GeocodeRaw(ctx, nil, address.Raw)
			if err != nil {
				return result, fmt.Errorf("Failed to geocode raw: %w", err)
			}
			addr = &geo_res.Address
		} else {
			return result, fmt.Errorf("empty address")
		}
	}

	saved_images, err := saveImageUploads(ctx, txn, images)
	if err != nil {
		return result, fmt.Errorf("Failed to save image uploads: %w", err)
	}
	if organization_id == 0 {
		organization_id, err = matchDistrict(ctx, location, images, addr)
		if err != nil {
			log.Warn().Err(err).Msg("Failed to match district")
		}
	}
	setter_report.OrganizationID = organization_id

	contact, err := ContactEmptyForOrganization(ctx, txn, organization_id)
	if err != nil {
		return result, fmt.Errorf("contact empty: %w", err)
	}
	setter_report.ReporterContactID = contact.ID
	if addr != nil {
		setter_report.AddressID = addr.ID
	}
	result, err = querypublicreport.ReportInsert(ctx, txn, setter_report)
	if err != nil {
		return result, fmt.Errorf("Failed to create report database record: %w", err)
	}
	if location != nil {
		l := *location
		if l.Latitude != 0 && l.Longitude != 0 {
			lint.LogOnErrCtx(func(ctx context.Context) error {
				return publicReportUpdateLocation(ctx, txn, result.ID, l)
			}, ctx, "update location")
		}
	}
	log.Info().Str("public_id", setter_report.PublicID).Int32("id", result.ID).Msg("Created base report")

	if len(saved_images) > 0 {
		setters := make([]modelpublicreport.ReportImage, len(saved_images))
		for i, image := range saved_images {
			setters[i] = modelpublicreport.ReportImage{
				ImageID:  int32(image.ID),
				ReportID: int32(result.ID),
			}
		}
		_, err = querypublicreport.ReportImagesInsert(ctx, txn, setters)
		if err != nil {
			return result, fmt.Errorf("Failed to save reference to images: %w", err)
		}
		log.Info().Int("len", len(images)).Msg("saved uploaded images")
	}

	err = detail_setter(ctx, txn, result.ID)
	if err != nil {
		return result, fmt.Errorf("detail setter: %w", err)
	}

	_, err = querypublicreport.ReportLogInsert(ctx, txn, modelpublicreport.ReportLog{
		Created:    time.Now(),
		EmailLogID: nil,
		// ID
		ReportID:  result.ID,
		TextLogID: nil,
		Type:      modelpublicreport.Reportlogtype_Created,
		UserID:    nil,
	})

	// Only create communication entries for compliance when they're submitted
	report_type := setter_report.ReportType
	var comm_id int
	if report_type != modelpublicreport.Reporttype_Compliance {
		comm := model.Communication{
			Created: time.Now(),

			OrganizationID:     result.OrganizationID,
			ResponseEmailLogID: nil,
			ResponseTextLogID:  nil,
			SourceEmailLogID:   nil,
			SourceReportID:     &result.ID,
			SourceTextLogID:    nil,
			Status:             model.Communicationstatus_New,
		}
		comm, err = querypublic.CommunicationInsert(ctx, txn, comm)
		if err != nil {
			return result, fmt.Errorf("insert communication: %w", err)
		}
		log.Debug().Int32("id", comm.ID).Msg("inserted new communication")
		comm_id = int(comm.ID)
	}

	if err := txn.Commit(ctx); err != nil {
		return result, fmt.Errorf("commit: %w", err)
	}

	event.Created(
		event.TypeRMOPublicReport,
		organization_id,
		result.PublicID,
	)
	if report_type != modelpublicreport.Reporttype_Compliance && comm_id != 0 {
		event.Created(
			event.TypeCommunication,
			organization_id,
			strconv.Itoa(comm_id),
		)
	}
	return result, nil
}
func publicReportUpdateAddressID(ctx context.Context, txn db.Tx, report *modelpublicreport.Report, address types.Address) error {
	var err error
	if address.GID == "" && address.Raw != "" {
		geo_res, err := geocode.GeocodeRaw(ctx, nil, address.Raw)
		if err != nil {
			return fmt.Errorf("Failed to geocode raw: %w", err)
		}
		statement := tablepublicreport.Report.UPDATE(
			tablepublicreport.Report.AddressID,
		).SET(
			tablepublicreport.Report.AddressID.SET(postgres.Int(int64(*geo_res.Address.ID))),
		).WHERE(
			tablepublicreport.Report.ID.EQ(postgres.Int(int64(report.ID))),
		)
		err = db.ExecuteNoneTx(ctx, txn, statement)
	} else {
		statement := tablepublicreport.Report.UPDATE(
			tablepublicreport.Report.AddressID,
		).SET(
			tablepublic.Address.SELECT(
				tablepublic.Address.ID,
			).WHERE(
				tablepublic.Address.Gid.EQ(postgres.String(address.GID)),
			).LIMIT(1),
		).WHERE(
			tablepublicreport.Report.ID.EQ(postgres.Int(int64(report.ID))),
		)
		err = db.ExecuteNoneTx(ctx, txn, statement)
	}
	if err != nil {
		return fmt.Errorf("update report address_id: %w", err)
	}
	return nil
}
func publicReportUpdateLocation(ctx context.Context, txn db.Tx, id int32, location types.Location) error {
	h3cell, _ := location.H3Cell()
	if h3cell == nil {
		return fmt.Errorf("nil h3 cell")
	}
	geom_query, _ := location.GeometryQuery()
	statement := tablepublicreport.Report.UPDATE(
		tablepublicreport.Report.H3cell,
		tablepublicreport.Report.Location,
	).SET(
		postgres.Int(int64(*h3cell)),
		postgres.Raw(geom_query),
	).WHERE(
		tablepublicreport.Report.ID.EQ(postgres.Int(int64(id))),
	)
	err := db.ExecuteNoneTx(ctx, txn, statement)
	if err != nil {
		return fmt.Errorf("Failed to insert publicreport.report geospatial", err)
	}
	return nil
}
