package com.andr3h3nriqu3s.applications

import io.opentelemetry.api.OpenTelemetry
import io.opentelemetry.api.trace.SpanKind
import java.sql.ResultSet
import java.util.UUID
import kotlin.collections.emptyList
import kotlin.collections.setOf
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.jdbc.core.RowMapper
import org.springframework.stereotype.Service
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestHeader
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.server.ResponseStatusException

data class ApplicationUrl(
        var application_id: String,
        var url: String,
) {
    companion object : RowMapper<ApplicationUrl> {
        override public fun mapRow(rs: ResultSet, rowNum: Int): ApplicationUrl {
            return ApplicationUrl(
                    rs.getString("application_id"),
                    rs.getString("url"),
            )
        }
    }
}

data class Application(
        var id: String,
        var url: String,
        var title: String,
        var user_id: String,
        var extra_data: String,
        var payrange: String,
        var status_id: String?,
        var company: String,
        var recruiter: String,
        var agency: Boolean,
        var message: String,
        var create_time: String,
        var simple_url: String,
        var job_level: String,
        var inperson_type: String,
        var location: String,
        var flairs: List<Flair>,
        var events: List<Event>,
        var urls: List<String>,
) {
    companion object : RowMapper<Application> {
        override public fun mapRow(rs: ResultSet, rowNum: Int): Application {
            return Application(
                    rs.getString("id"),
                    rs.getString("url"),
                    rs.getString("title"),
                    rs.getString("user_id"),
                    rs.getString("extra_data"),
                    rs.getString("payrange"),
                    rs.getString("status_id"),
                    rs.getString("company"),
                    rs.getString("recruiter"),
                    rs.getBoolean("agency"),
                    rs.getString("message"),
                    rs.getString("create_time"),
                    rs.getString("simple_url"),
                    rs.getString("job_level"),
                    rs.getString("inperson_type"),
                    rs.getString("location"),
                    emptyList(),
                    emptyList(),
                    emptyList(),
            )
        }
    }
}

data class SubmitRequest(val text: String)

data class StatusRequest(val id: String, val status_id: String?)

data class FlairRequest(val id: String, val text: String)

data class UpdateUrl(val id: String, val url: String)

data class CVData(
        val company: String,
        val recruiter: String,
        val message: String,
        val agency: Boolean,
        val flairs: List<SimpleFlair>
)

@RestController
@ControllerAdvice
@RequestMapping("/api/application")
class ApplicationsController(
        val sessionService: SessionService,
        val applicationService: ApplicationService,
        val flairService: FlairService,
        val eventService: EventService,
        val openTelemetry: OpenTelemetry
) {

    @GetMapping(path = ["/cv/{id}"], produces = [MediaType.APPLICATION_JSON_VALUE])
    public fun getCV(@PathVariable id: String, @RequestHeader("token") token: String?): CVData? {
        val user = sessionService.verifyToken(token)

        val application = applicationService.findApplicationByIdNoUser(id)

        if (application == null) return null

        if (user == null) {
            eventService.create(application.id, EventType.View)
        }

        val flairs = application.flairs.filter { it.sort != -1 }.map { it.toFlairSimple() }

        return CVData(
                application.company,
                application.recruiter,
                application.message,
                application.agency,
                flairs
        )
    }

    /** Create a new application from the link */
    @PostMapping(path = ["/link"], produces = [MediaType.APPLICATION_JSON_VALUE])
    public fun submitLink(
            @RequestBody submit: SubmitRequest,
            @RequestHeader("token") token: String
    ): Application {
        val user = sessionService.verifyTokenThrow(token)

        var application =
                Application(
                        UUID.randomUUID().toString(),
                        submit.text,
                        "New Application",
                        user.id,
                        "",
                        "",
                        null,
                        "",
                        "",
                        false,
                        "",
                        "",
                        "",
                        "",
                        "",
                        "",
                        emptyList(),
                        emptyList(),
                        emptyList(),
                )

        if (!applicationService.createApplication(user, application)) {
            throw ResponseStatusException(HttpStatus.CONFLICT, "Application already exists", null)
        }

        print("Created: ")
        println(application)

        return application
    }

    @PostMapping(path = ["/text/flair"], produces = [MediaType.APPLICATION_JSON_VALUE])
    public fun textFlair(
            @RequestBody info: FlairRequest,
            @RequestHeader("token") token: String
    ): Int {
        val trace = openTelemetry.getTracer("applications")

        val user = sessionService.verifyTokenThrow(token)
        val application = applicationService.findApplicationById(user, info.id)
        if (application == null) {
            throw ResponseStatusException(HttpStatus.NOT_FOUND, "Application not found", null)
        }

        val flairs = flairService.listUser(user)

        var count = 0

        val span = trace.spanBuilder("parse-doc").setSpanKind(SpanKind.INTERNAL).startSpan()
        for (flair: Flair in flairs) {
            val regex =
                    Regex(
                            ".*" + flair.expr + ".*",
                            setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL)
                    )

            if (regex.matches(info.text)) {
                count += 1
                flairService.linkFlair(application, flair)
            }
        }
        span.end()

        return count
    }

    @PostMapping(path = ["/text"], produces = [MediaType.APPLICATION_JSON_VALUE])
    public fun submitText(
            @RequestBody submit: SubmitRequest,
            @RequestHeader("token") token: String
    ): List<Application> {
        val user = sessionService.verifyTokenThrow(token)
        return applicationService.parseText(user, submit.text)
    }

    @GetMapping(path = ["/list"], produces = [MediaType.APPLICATION_JSON_VALUE])
    public fun list(@RequestHeader("token") token: String): List<Application> {
        val user = sessionService.verifyTokenThrow(token)
        return applicationService.findAll(user)
    }

    @GetMapping(path = ["/{id}"], produces = [MediaType.APPLICATION_JSON_VALUE])
    public fun get(@PathVariable id: String, @RequestHeader("token") token: String): Application? {
        val user = sessionService.verifyTokenThrow(token)
        val app = applicationService.findApplicationById(user, id)
        if (app == null) {
            throw ResponseStatusException(HttpStatus.NOT_FOUND, "Application not found", null)
        }
        return app
    }

    @PostMapping(
            path = ["/link/application/{toLink}/{surviving}"],
            produces = [MediaType.APPLICATION_JSON_VALUE]
    )
    public fun get(
            @PathVariable toLink: String,
            @PathVariable surviving: String,
            @RequestHeader("token") token: String
    ): Application {
        val user = sessionService.verifyTokenThrow(token)
        val toLinkApp = applicationService.findApplicationById(user, toLink)
        val app = applicationService.findApplicationById(user, surviving)

        if (app == null || toLinkApp == null) {
            throw ResponseStatusException(HttpStatus.NOT_FOUND, "Application not found", null)
        }

        applicationService.linkApplications(toLinkApp, app)
        applicationService.delete(toLinkApp)

        return app
    }

    @PutMapping(path = ["/status"], produces = [MediaType.APPLICATION_JSON_VALUE])
    public fun status(
            @RequestBody info: StatusRequest,
            @RequestHeader("token") token: String,
    ): Application {
        val user = sessionService.verifyTokenThrow(token)
        var application = applicationService.findApplicationById(user, info.id)
        if (application == null) {
            throw NotFound()
        }

        if (application.status_id == info.status_id) {
            return application
        }

        application.status_id = info.status_id

        applicationService.updateStatus(application)

        return application
    }

    @PutMapping(path = ["/update"], produces = [MediaType.APPLICATION_JSON_VALUE])
    public fun update(
            @RequestBody info: Application,
            @RequestHeader("token") token: String
    ): Application {
        val user = sessionService.verifyTokenThrow(token)
        var application = applicationService.findApplicationById(user, info.id)
        if (application == null) {
            throw NotFound()
        }
        applicationService.update(info)
        return info
    }

    @PostMapping(path = ["/update/url"], produces = [MediaType.APPLICATION_JSON_VALUE])
    public fun updateUrl(
            @RequestBody info: UpdateUrl,
            @RequestHeader("token") token: String
    ): Application {
        val user = sessionService.verifyTokenThrow(token)
        var application = applicationService.findApplicationById(user, info.id)

        if (application == null) {
            throw ResponseStatusException(HttpStatus.NOT_FOUND, "Application not found", null)
        }

        application.url = info.url
        application.simple_url = info.url.split("?")[0]

        var maybe_exists =
                applicationService.findApplicationByUrl(user, application.url)
                        ?: applicationService.findApplicationByUrl(user, application.simple_url)

        if (maybe_exists != null && maybe_exists.id != application.id) {
            applicationService.delete(application)

            maybe_exists.flairs = flairService.listFromLinkApplicationId(maybe_exists.id)
            maybe_exists.events = eventService.listFromApplicationId(maybe_exists.id).toList()

            return maybe_exists
        }

        applicationService.addUrl(application.id, info.url)
        applicationService.addUrl(application.id, info.url.split("?")[0])
        applicationService.update(application)

        application.flairs = flairService.listFromLinkApplicationId(application.id)
        application.events = eventService.listFromApplicationId(application.id).toList()

        return application
    }

    @DeleteMapping(path = ["/flair/{id}/{flairid}"], produces = [MediaType.APPLICATION_JSON_VALUE])
    public fun delete(
            @PathVariable id: String,
            @PathVariable flairid: String,
            @RequestHeader("token") token: String
    ): Application {
        val user = sessionService.verifyTokenThrow(token)

        val application = applicationService.findApplicationById(user, id)
        if (application == null) {
            throw NotFound()
        }

        flairService.unlinkFlair(id, flairid)
        return applicationService.findApplicationById(user, id)!!
    }

    @DeleteMapping(path = ["/{id}"], produces = [MediaType.APPLICATION_JSON_VALUE])
    public fun delete(
            @PathVariable id: String,
            @RequestHeader("token") token: String
    ): Application {
        val user = sessionService.verifyTokenThrow(token)

        val application = applicationService.findApplicationById(user, id)
        if (application == null) {
            throw NotFound()
        }

        applicationService.delete(application)
        return application
    }
}

@Service
class ApplicationService(
        val db: JdbcTemplate,
        val flairService: FlairService,
        val eventService: EventService,
) {

    public fun findApplicationByUrl(user: UserDb, url: String): Application? {
        val applications =
                db.query(
                                "select * from applications as app inner join applications_urls as app_url on app_url.application_id=app.id where app_url.url=? and user_id=?",
                                arrayOf(url, user.id),
                                Application
                        )
                        .toList()

        if (applications.size == 0) {
            return null
        }

        return applications[0]
    }

    public fun findApplicationById(user: UserDb, id: String): Application? {
        var applications =
                db.query(
                                "select * from applications where id=? and user_id=?",
                                arrayOf(id, user.id),
                                Application
                        )
                        .toList()

        if (applications.size == 0) {
            return null
        }

        var application = applications[0]

        application.flairs = flairService.listFromLinkApplicationId(application.id)
        application.events = eventService.listFromApplicationId(application.id).toList()
        application.urls = this.getUrls(application)
        return application
    }

    public fun findApplicationByIdNoUser(id: String): Application? {
        var applications =
                db.query("select * from applications where id=?", arrayOf(id), Application).toList()

        if (applications.size == 0) {
            return null
        }

        var application = applications[0]

        // Views / Events are not needed for this request
        application.flairs = flairService.listFromLinkApplicationId(application.id)

        return application
    }

    public fun addUrl(id: String, url: String) {
        val applications =
                db.query(
                                "select * from applications_urls as app_url where app_url.url=? and app_url.application_id=?",
                                arrayOf(url, id),
                                ApplicationUrl
                        )
                        .toList()

        if (applications.size > 0) {
            return
        }

        db.update("insert into applications_urls (application_id, url) values (?, ?);", id, url)
    }

    public fun parseText(user: UserDb, textIn: String): List<Application> {
        var text = textIn.replace("=\n", "")

        var urls: List<String> = emptyList()

        while (true) {
            var index = text.indexOf("href")

            if (index == -1) {
                break
            }

            var new_url = StringBuilder()

            var found_start = false

            while (true) {
                if (found_start) {
                    if (text[index] == '"') {
                        break
                    }
                    new_url.append(text[index])
                } else if (text[index] == '"') {
                    found_start = true
                }
                index++
            }

            text = text.substring(index)

            urls = urls.plus(new_url.toString().replace("&amp;", "&").replace("=3D", "="))
        }

        print("found: ")
        print(urls.size)
        print(" links\n")

        // jobListing is for glassdoor urls
        // jobs/view is for linkeding urls
        urls =
                urls.filter { predicate ->
                    print("url: ")
                    print(predicate)
                    print("\n")
                    predicate.contains("jobListing") || predicate.contains("jobs/view")
                }

        print("found fileted: ")
        print(urls.size)
        print(" links\n")

        urls = urls.toSet().toList()

        print("removed duplicates: ")
        print(urls.size)
        print(" links\n")

        var applications =
                urls.map { elm ->
                    Application(
                            UUID.randomUUID().toString(),
                            if (elm.contains("linkedin")) elm.split("?")[0] else elm,
                            "New Application",
                            user.id,
                            "",
                            "",
                            null,
                            "",
                            "",
                            false,
                            "",
                            "",
                            if (elm.contains("linkedin")) elm.split("?")[0] else "",
                            "",
                            "",
                            "",
                            emptyList(),
                            emptyList(),
                            emptyList(),
                    )
                }

        applications = applications.filter { elm -> this.createApplication(user, elm) }

        print("created new: ")
        print(applications.size)
        print(" links\n")

        return applications
    }

    public fun createApplication(user: UserDb, application: Application): Boolean {
        if (this.findApplicationByUrl(user, application.url) != null) {
            return false
        }

        // Create time is auto created by the database
        db.update(
                "insert into applications (id, url, title, user_id, extra_data, payrange, status_id, company, recruiter, message, agency, simple_url, job_level, inperson_type, location) values (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);",
                application.id,
                application.url,
                application.title,
                application.user_id,
                application.extra_data,
                application.payrange,
                application.status_id,
                application.company,
                application.recruiter,
                application.message,
                application.agency,
                application.simple_url,
                application.job_level,
                application.inperson_type,
                application.location,
        )

        eventService.create(application.id, EventType.Creation)
        addUrl(application.id, application.url)

        return true
    }

    private fun internalFindAll(user: UserDb): Iterable<Application> {
        return db.query(
                "select * from applications where user_id=? order by title asc;",
                arrayOf(user.id),
                Application
        )
    }

    public fun findAll(user: UserDb): List<Application> {
        var iter = internalFindAll(user)
        return iter.toList()
    }

    public fun findAllByUserStatusId(statusId: String, user: UserDb): List<Application> {
        return db.query(
                "select * from applications where status_id=? and user_id=? order by title asc;",
                arrayOf(statusId, user.id),
                Application
        )
    }

    // Update the stauts on the application object before giving it to this function
    // TODO how status history works
    public fun updateStatus(application: Application): Application {

        eventService.create(application.id, EventType.StatusUpdate, application.status_id)

        db.update(
                "update applications set status_id=? where id=?",
                application.status_id,
                application.id,
        )
        return application
    }

    // Note this does not update status
    public fun update(application: Application): Application {
        // I don't want ot update create_time
        db.update(
                "update applications set url=?, title=?, user_id=?, extra_data=?, payrange=?, company=?, recruiter=?, message=?, agency=?, simple_url=?, job_level=?, inperson_type=?, location=? where id=?",
                application.url,
                application.title,
                application.user_id,
                application.extra_data,
                application.payrange,
                application.company,
                application.recruiter,
                application.message,
                application.agency,
                application.simple_url,
                application.job_level,
                application.inperson_type,
                application.location,
                application.id,
        )
        return application
    }

    public fun linkApplications(toLink: Application, surviving: Application) {
        db.update(
                "update applications_urls set application_id=? where application_id=?;",
                surviving.id,
                toLink.id,
        )
    }

    public fun getUrls(toGet: Application): List<String> {
        var urls =
                db.query(
                        "select * from applications_urls where application_id=?",
                        arrayOf(toGet.id),
                        ApplicationUrl
                )
        return urls.map { it.url }
    }

    public fun delete(application: Application) {
        db.update(
                "delete from applications where id=?",
                application.id,
        )
        db.update(
                "delete from applications_urls where application_id=?",
                application.id,
        )
        db.update(
                "delete from events where application_id=?",
                application.id,
        )
        db.update(
                "delete from flair_link where application_id=?",
                application.id,
        )
    }
}
