from datetime import datetime
from html import unescape
from pathlib import Path
import re
import sys

import psycopg2
from psycopg2.extras import execute_values


ROOT_DIR = Path(__file__).resolve().parents[1]
CONN_DIR = ROOT_DIR / "conn"
if str(CONN_DIR) not in sys.path:
    sys.path.insert(0, str(CONN_DIR))

import config
import mysql5


TAG_RE = re.compile(r"<[^>]+>")
SPACE_RE = re.compile(r"\s+")
CODE_RE = re.compile(r"\b[A-Z]{1,8}[-_/]?\d{2,}[A-Z0-9._/-]*\b|\b[A-Z0-9_]{3,}\.[A-Z0-9_.$-]+\b", re.IGNORECASE)


def get_pg_connection():
    return psycopg2.connect(**config.POSTGRES_CONFIG)


def clean_text(value):
    if value is None:
        return ""
    text = unescape(str(value))
    text = TAG_RE.sub(" ", text)
    text = text.replace("\x00", " ")
    return SPACE_RE.sub(" ", text).strip()


def short_text(value, max_len=420):
    text = clean_text(value)
    if len(text) <= max_len:
        return text
    return text[:max_len].rsplit(" ", 1)[0] + "..."


def extract_codes(*values):
    codes = set()
    for value in values:
        for match in CODE_RE.findall(clean_text(value).upper()):
            codes.add(match.strip(".,;:()[]{}"))
    return sorted(code for code in codes if code)


def build_index_document(row):
    subject = clean_text(row.get("subject"))
    problem = clean_text(row.get("problem"))
    solution = clean_text(row.get("solution"))
    metadata = " ".join(
        clean_text(row.get(name))
        for name in ("competence", "product", "area", "subarea", "client", "gravity", "status", "program", "program_type")
        if row.get(name) is not None
    )
    codes = extract_codes(subject, problem, solution, metadata)
    search_text = " ".join(part for part in (subject, problem, solution, metadata, " ".join(codes)) if part)
    return {
        "ticket_id": int(row["ticket_id"]),
        "subject_text": subject,
        "problem_text": problem,
        "solution_text": solution,
        "solution_preview": short_text(solution),
        "metadata_text": metadata,
        "search_text": search_text,
        "codes": codes,
    }


def ensure_ai_schema():
    connection = get_pg_connection()
    cursor = connection.cursor()

    cursor.execute("CREATE EXTENSION IF NOT EXISTS pg_trgm")

    cursor.execute(
        """
        CREATE TABLE IF NOT EXISTS ticket_ai_index (
            ticket_id BIGINT PRIMARY KEY,
            subject_text TEXT NOT NULL DEFAULT '',
            problem_text TEXT NOT NULL DEFAULT '',
            solution_text TEXT NOT NULL DEFAULT '',
            solution_preview TEXT NOT NULL DEFAULT '',
            metadata_text TEXT NOT NULL DEFAULT '',
            search_text TEXT NOT NULL DEFAULT '',
            codes TEXT[] NOT NULL DEFAULT '{}',
            competence_id INTEGER,
            competence TEXT,
            product_id INTEGER,
            product TEXT,
            area_id INTEGER,
            area TEXT,
            subarea_id INTEGER,
            subarea TEXT,
            client_id INTEGER,
            client TEXT,
            gravity_id INTEGER,
            gravity TEXT,
            status_id INTEGER,
            status TEXT,
            opened_at TIMESTAMP,
            closed_at TIMESTAMP,
            program TEXT,
            program_type TEXT,
            version_id INTEGER,
            patch_id INTEGER,
            classification_id INTEGER,
            os_id INTEGER,
            search_vector TSVECTOR,
            source_updated_at TIMESTAMP,
            indexed_at TIMESTAMP NOT NULL DEFAULT now()
        )
        """
    )

    cursor.execute(
        """
        CREATE OR REPLACE FUNCTION ticket_ai_index_search_vector_update()
        RETURNS trigger AS $$
        BEGIN
            NEW.search_vector :=
                setweight(to_tsvector('simple', COALESCE(NEW.subject_text, '')), 'A') ||
                setweight(to_tsvector('simple', COALESCE(NEW.problem_text, '')), 'A') ||
                setweight(to_tsvector('simple', COALESCE(NEW.solution_text, '')), 'B') ||
                setweight(to_tsvector('simple', COALESCE(NEW.metadata_text, '')), 'C') ||
                setweight(to_tsvector('simple', array_to_string(COALESCE(NEW.codes, '{}'), ' ')), 'A');
            RETURN NEW;
        END
        $$ LANGUAGE plpgsql
        """
    )

    cursor.execute(
        """
        DROP TRIGGER IF EXISTS ticket_ai_index_search_vector_trigger ON ticket_ai_index
        """
    )
    cursor.execute(
        """
        CREATE TRIGGER ticket_ai_index_search_vector_trigger
        BEFORE INSERT OR UPDATE ON ticket_ai_index
        FOR EACH ROW EXECUTE FUNCTION ticket_ai_index_search_vector_update()
        """
    )

    cursor.execute("CREATE INDEX IF NOT EXISTS ticket_ai_index_search_vector_idx ON ticket_ai_index USING GIN (search_vector)")
    cursor.execute("CREATE INDEX IF NOT EXISTS ticket_ai_index_problem_trgm_idx ON ticket_ai_index USING GIN (problem_text gin_trgm_ops)")
    cursor.execute("CREATE INDEX IF NOT EXISTS ticket_ai_index_subject_trgm_idx ON ticket_ai_index USING GIN (subject_text gin_trgm_ops)")
    cursor.execute("CREATE INDEX IF NOT EXISTS ticket_ai_index_codes_idx ON ticket_ai_index USING GIN (codes)")
    cursor.execute("CREATE INDEX IF NOT EXISTS ticket_ai_index_product_idx ON ticket_ai_index (product_id)")
    cursor.execute("CREATE INDEX IF NOT EXISTS ticket_ai_index_area_idx ON ticket_ai_index (area_id)")
    cursor.execute("CREATE INDEX IF NOT EXISTS ticket_ai_index_subarea_idx ON ticket_ai_index (subarea_id)")
    cursor.execute("CREATE INDEX IF NOT EXISTS ticket_ai_index_competence_idx ON ticket_ai_index (competence_id)")
    cursor.execute("CREATE INDEX IF NOT EXISTS ticket_ai_index_opened_idx ON ticket_ai_index (opened_at)")

    cursor.execute(
        """
        CREATE TABLE IF NOT EXISTS ticket_ai_query_log (
            id BIGSERIAL PRIMARY KEY,
            created_at TIMESTAMP NOT NULL DEFAULT now(),
            user_id BIGINT,
            query_ticket_id BIGINT NOT NULL,
            suggested_ticket_id BIGINT NOT NULL,
            rank INTEGER NOT NULL,
            total_score DOUBLE PRECISION NOT NULL,
            lexical_score DOUBLE PRECISION NOT NULL DEFAULT 0,
            trigram_score DOUBLE PRECISION NOT NULL DEFAULT 0,
            code_score DOUBLE PRECISION NOT NULL DEFAULT 0,
            metadata_score DOUBLE PRECISION NOT NULL DEFAULT 0,
            feedback_score DOUBLE PRECISION NOT NULL DEFAULT 0,
            confidence TEXT NOT NULL,
            query_text TEXT NOT NULL,
            reasons TEXT[] NOT NULL DEFAULT '{}'
        )
        """
    )
    cursor.execute("CREATE INDEX IF NOT EXISTS ticket_ai_query_log_query_idx ON ticket_ai_query_log (query_ticket_id)")
    cursor.execute("CREATE INDEX IF NOT EXISTS ticket_ai_query_log_suggestion_idx ON ticket_ai_query_log (suggested_ticket_id)")
    cursor.execute("ALTER TABLE ticket_ai_query_log ADD COLUMN IF NOT EXISTS code_score DOUBLE PRECISION NOT NULL DEFAULT 0")

    cursor.execute(
        """
        CREATE TABLE IF NOT EXISTS ticket_ai_search_log (
            id BIGSERIAL PRIMARY KEY,
            created_at TIMESTAMP NOT NULL DEFAULT now(),
            user_id BIGINT,
            query_kind TEXT NOT NULL DEFAULT 'ticket',
            query_ticket_id BIGINT,
            query_text TEXT NOT NULL DEFAULT '',
            filters JSONB NOT NULL DEFAULT '{}'::jsonb,
            result_count INTEGER NOT NULL DEFAULT 0,
            elapsed_ms INTEGER NOT NULL DEFAULT 0
        )
        """
    )
    cursor.execute("CREATE INDEX IF NOT EXISTS ticket_ai_search_log_created_idx ON ticket_ai_search_log (created_at)")
    cursor.execute("CREATE INDEX IF NOT EXISTS ticket_ai_search_log_result_idx ON ticket_ai_search_log (result_count)")
    cursor.execute("CREATE INDEX IF NOT EXISTS ticket_ai_search_log_ticket_idx ON ticket_ai_search_log (query_ticket_id)")

    cursor.execute(
        """
        CREATE TABLE IF NOT EXISTS ticket_ai_feedback (
            id BIGSERIAL PRIMARY KEY,
            created_at TIMESTAMP NOT NULL DEFAULT now(),
            user_id BIGINT,
            query_ticket_id BIGINT NOT NULL,
            suggested_ticket_id BIGINT NOT NULL,
            feedback TEXT NOT NULL CHECK (feedback IN ('useful', 'not_useful', 'favorite', 'opened')),
            note TEXT
        )
        """
    )
    cursor.execute("CREATE INDEX IF NOT EXISTS ticket_ai_feedback_pair_idx ON ticket_ai_feedback (query_ticket_id, suggested_ticket_id)")
    cursor.execute("CREATE INDEX IF NOT EXISTS ticket_ai_feedback_suggestion_idx ON ticket_ai_feedback (suggested_ticket_id)")

    cursor.execute(
        """
        CREATE MATERIALIZED VIEW IF NOT EXISTS ticket_ai_feedback_summary AS
        SELECT
            suggested_ticket_id AS ticket_id,
            SUM(CASE WHEN feedback IN ('useful', 'favorite', 'opened') THEN 1 ELSE 0 END) AS positive_count,
            SUM(CASE WHEN feedback = 'not_useful' THEN 1 ELSE 0 END) AS negative_count
        FROM ticket_ai_feedback
        GROUP BY suggested_ticket_id
        """
    )
    cursor.execute("CREATE UNIQUE INDEX IF NOT EXISTS ticket_ai_feedback_summary_ticket_idx ON ticket_ai_feedback_summary (ticket_id)")

    cursor.execute(
        """
        CREATE TABLE IF NOT EXISTS ticket_bot_user_settings (
            user_id BIGINT PRIMARY KEY,
            notifications_enabled BOOLEAN NOT NULL DEFAULT TRUE,
            quiet_start_hour INTEGER,
            quiet_end_hour INTEGER,
            digest_minutes INTEGER NOT NULL DEFAULT 0,
            high_gravity_only BOOLEAN NOT NULL DEFAULT FALSE,
            last_notification_at TIMESTAMP,
            updated_at TIMESTAMP NOT NULL DEFAULT now(),
            CHECK (quiet_start_hour IS NULL OR (quiet_start_hour >= 0 AND quiet_start_hour <= 23)),
            CHECK (quiet_end_hour IS NULL OR (quiet_end_hour >= 0 AND quiet_end_hour <= 23)),
            CHECK (digest_minutes >= 0)
        )
        """
    )

    connection.commit()
    cursor.close()
    connection.close()


def _row_values(row):
    doc = build_index_document(row)
    return (
        doc["ticket_id"],
        doc["subject_text"],
        doc["problem_text"],
        doc["solution_text"],
        doc["solution_preview"],
        doc["metadata_text"],
        doc["search_text"],
        doc["codes"],
        row.get("competence_id"),
        row.get("competence"),
        row.get("product_id"),
        row.get("product"),
        row.get("area_id"),
        row.get("area"),
        row.get("subarea_id"),
        row.get("subarea"),
        row.get("client_id"),
        row.get("client"),
        row.get("gravity_id"),
        row.get("gravity"),
        row.get("status_id"),
        row.get("status"),
        row.get("opened_at"),
        row.get("closed_at"),
        row.get("program"),
        row.get("program_type"),
        row.get("version_id"),
        row.get("patch_id"),
        row.get("classification_id"),
        row.get("os_id"),
        datetime.utcnow(),
    )


def upsert_ticket_batch(rows):
    if not rows:
        return 0

    values = [_row_values(row) for row in rows]
    connection = get_pg_connection()
    cursor = connection.cursor()
    execute_values(
        cursor,
        """
        INSERT INTO ticket_ai_index (
            ticket_id,
            subject_text,
            problem_text,
            solution_text,
            solution_preview,
            metadata_text,
            search_text,
            codes,
            competence_id,
            competence,
            product_id,
            product,
            area_id,
            area,
            subarea_id,
            subarea,
            client_id,
            client,
            gravity_id,
            gravity,
            status_id,
            status,
            opened_at,
            closed_at,
            program,
            program_type,
            version_id,
            patch_id,
            classification_id,
            os_id,
            source_updated_at
        ) VALUES %s
        ON CONFLICT (ticket_id) DO UPDATE SET
            subject_text = EXCLUDED.subject_text,
            problem_text = EXCLUDED.problem_text,
            solution_text = EXCLUDED.solution_text,
            solution_preview = EXCLUDED.solution_preview,
            metadata_text = EXCLUDED.metadata_text,
            search_text = EXCLUDED.search_text,
            codes = EXCLUDED.codes,
            competence_id = EXCLUDED.competence_id,
            competence = EXCLUDED.competence,
            product_id = EXCLUDED.product_id,
            product = EXCLUDED.product,
            area_id = EXCLUDED.area_id,
            area = EXCLUDED.area,
            subarea_id = EXCLUDED.subarea_id,
            subarea = EXCLUDED.subarea,
            client_id = EXCLUDED.client_id,
            client = EXCLUDED.client,
            gravity_id = EXCLUDED.gravity_id,
            gravity = EXCLUDED.gravity,
            status_id = EXCLUDED.status_id,
            status = EXCLUDED.status,
            opened_at = EXCLUDED.opened_at,
            closed_at = EXCLUDED.closed_at,
            program = EXCLUDED.program,
            program_type = EXCLUDED.program_type,
            version_id = EXCLUDED.version_id,
            patch_id = EXCLUDED.patch_id,
            classification_id = EXCLUDED.classification_id,
            os_id = EXCLUDED.os_id,
            source_updated_at = EXCLUDED.source_updated_at,
            indexed_at = now()
        """,
        values,
        page_size=500,
    )
    connection.commit()
    cursor.close()
    connection.close()
    return len(rows)


def rebuild_index(batch_size=1000, max_rows=None):
    ensure_ai_schema()
    last_ticket = 0
    indexed = 0

    while True:
        rows = mysql5.fetch_solved_ticket_batch(last_ticket=last_ticket, limit=batch_size)
        if not rows:
            break
        upsert_ticket_batch(rows)
        indexed += len(rows)
        last_ticket = int(rows[-1]["ticket_id"])
        print(f"Indexed {indexed} tickets, last ticket {last_ticket}")
        if max_rows is not None and indexed >= max_rows:
            break

    refresh_feedback_summary()
    return indexed


def refresh_feedback_summary():
    connection = get_pg_connection()
    cursor = connection.cursor()
    cursor.execute("REFRESH MATERIALIZED VIEW ticket_ai_feedback_summary")
    connection.commit()
    cursor.close()
    connection.close()


if __name__ == "__main__":
    batch_size = int(sys.argv[1]) if len(sys.argv) > 1 else 1000
    max_rows = int(sys.argv[2]) if len(sys.argv) > 2 else None
    print(f"Starting AI index rebuild with batch_size={batch_size}, max_rows={max_rows}")
    total = rebuild_index(batch_size=batch_size, max_rows=max_rows)
    print(f"Finished indexing {total} solved tickets")
